From 1e2ad1ea87db771680a96ded821231ba2db3d817 Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 00:14:17 +0300 Subject: [PATCH 1/6] Update slender fortress to the latest version --- addons/sourcemod/gamedata/sf2.txt | 22 + addons/sourcemod/scripting/include/dhooks.inc | 152 +- addons/sourcemod/scripting/include/sf2.inc | 644 +- addons/sourcemod/scripting/rytp_horror.sp | 12891 ++++++++-------- .../scripting/rytp_horror/adminmenu.sp | 1820 +-- .../sourcemod/scripting/rytp_horror/client.sp | 11852 +++++++------- .../sourcemod/scripting/rytp_horror/debug.sp | 320 +- .../scripting/rytp_horror/effects.sp | 550 +- .../scripting/rytp_horror/logging.sp | 52 +- .../sourcemod/scripting/rytp_horror/menus.sp | 1594 +- addons/sourcemod/scripting/rytp_horror/nav.sp | 1414 +- addons/sourcemod/scripting/rytp_horror/npc.sp | 5354 ++++--- .../scripting/rytp_horror/npc/npc_chaser.sp | 5028 +++--- .../scripting/rytp_horror/playergroups.sp | 1226 +- .../rytp_horror/playergroups/menus.sp | 1238 +- .../scripting/rytp_horror/profiles.sp | 2376 +-- .../rytp_horror/profiles/profile_chaser.sp | 1140 +- addons/sourcemod/scripting/rytp_horror/pvp.sp | 1162 +- .../scripting/rytp_horror/pvp/menus.sp | 122 +- .../scripting/rytp_horror/specialround.sp | 755 +- .../sourcemod/scripting/rytp_horror/stocks.sp | 1397 +- addons/sourcemod/scripting/spcomp.exe | Bin 345600 -> 403968 bytes .../sourcemod/translations/ru/sf2.phrases.txt | 30 + addons/sourcemod/translations/sf2.phrases.txt | 30 + 24 files changed, 25574 insertions(+), 25595 deletions(-) create mode 100644 addons/sourcemod/gamedata/sf2.txt diff --git a/addons/sourcemod/gamedata/sf2.txt b/addons/sourcemod/gamedata/sf2.txt new file mode 100644 index 0000000..aa60ffc --- /dev/null +++ b/addons/sourcemod/gamedata/sf2.txt @@ -0,0 +1,22 @@ +"Games" +{ + "tf" + { + "Offsets" + { + "CTFPlayer::WantsLagCompensationOnEntity" + { + "windows" "323" + "linux" "324" + "mac" "324" + } + + "CBaseEntity::ShouldTransmit" + { + "windows" "18" + "linux" "19" + "mac" "19" + } + } + } +} \ No newline at end of file diff --git a/addons/sourcemod/scripting/include/dhooks.inc b/addons/sourcemod/scripting/include/dhooks.inc index 5d26765..604b186 100644 --- a/addons/sourcemod/scripting/include/dhooks.inc +++ b/addons/sourcemod/scripting/include/dhooks.inc @@ -2,6 +2,7 @@ #endinput #endif #define _dhooks_included + enum ObjectValueType { ObjectValueType_Int = 0, @@ -18,11 +19,13 @@ enum ObjectValueType ObjectValueType_CharPtr, ObjectValueType_String }; + enum ListenType { ListenType_Created, ListenType_Deleted }; + enum ReturnType { ReturnType_Unknown, @@ -38,6 +41,7 @@ enum ReturnType ReturnType_CBaseEntity, ReturnType_Edict }; + enum HookParamType { HookParamType_Unknown, @@ -53,18 +57,21 @@ enum HookParamType HookParamType_Edict, HookParamType_Object }; + enum ThisPointerType { ThisPointer_Ignore, ThisPointer_CBaseEntity, ThisPointer_Address }; + enum HookType { HookType_Entity, HookType_GameRules, HookType_Raw }; + enum MRESReturn { MRES_ChangedHandled = -2, // Use changed values and return MRES_Handled @@ -74,63 +81,71 @@ enum MRESReturn MRES_Override, // call real function, but use my return value MRES_Supercede // skip real function; use my return value }; + enum DHookPassFlag { - DHookPass_ByVal = (1<<0), - DHookPass_ByRef = (1<<1) + DHookPass_ByVal = (1<<0), /**< Passing by value */ + DHookPass_ByRef = (1<<1), /**< Passing by reference */ + DHookPass_ODTOR = (1<<2), /**< Object has a destructor */ + DHookPass_OCTOR = (1<<3), /**< Object has a constructor */ + DHookPass_OASSIGNOP = (1<<4), /**< Object has an assignment operator */ }; -funcenum ListenCB + +typeset ListenCB { //Deleted - public (entity), + function void (int entity); + //Created - public (entity, const String:classname[]) -} -funcenum DHookRemovalCB + function void (int entity, const char[] classname); +}; + +typeset DHookRemovalCB { - public (hookid) + function void (int hookid); }; -funcenum DHookCallback +typeset DHookCallback { //Function Example: void Ham::Test() with this pointer ignore - MRESReturn:public(), + function MRESReturn (); //Function Example: void Ham::Test() with this pointer passed - MRESReturn:public(thisPointer), + function MRESReturn (int pThis); //Function Example: void Ham::Test(int cake) with this pointer ignore - MRESReturn:public(Handle:hParams), + function MRESReturn (Handle hParams); //Function Example: void Ham::Test(int cake) with this pointer passed - MRESReturn:public(thisPointer, Handle:hParams), + function MRESReturn (int pThis, Handle hParams); //Function Example: int Ham::Test() with this pointer ignore - MRESReturn:public(Handle:hReturn), + function MRESReturn (Handle hReturn); //Function Example: int Ham::Test() with this pointer passed - MRESReturn:public(thisPointer, Handle:hReturn), + function MRESReturn (int pThis, Handle hReturn); //Function Example: int Ham::Test(int cake) with this pointer ignore - MRESReturn:public(Handle:hReturn, Handle:hParams), + function MRESReturn (Handle hReturn, Handle hParams); //Function Example: int Ham::Test(int cake) with this pointer passed - MRESReturn:public(thisPointer, Handle:hReturn, Handle:hParams), + function MRESReturn (int pThis, Handle hReturn, Handle hParams); //Address NOW //Function Example: void Ham::Test() with this pointer passed - MRESReturn:public(Address:thisPointer), + function MRESReturn (Address pThis); //Function Example: void Ham::Test(int cake) with this pointer passed - MRESReturn:public(Address:thisPointer, Handle:hParams), + function MRESReturn (Address pThis, Handle hParams); //Function Example: int Ham::Test() with this pointer passed - MRESReturn:public(Address:thisPointer, Handle:hReturn), + function MRESReturn (Address pThis, Handle hReturn); //Function Example: int Ham::Test(int cake) with this pointer passed - MRESReturn:public(Address:thisPointer, Handle:hReturn, Handle:hParams) + function MRESReturn (Address pThis, Handle hReturn, Handle hParams); }; + /* Adds an entity listener hook * * @param type Type of listener to add @@ -138,7 +153,7 @@ funcenum DHookCallback * * @noreturn */ -native DHookAddEntityListener(ListenType:type, ListenCB:callback); +native void DHookAddEntityListener(ListenType type, ListenCB callback); /* Removes an entity listener hook * @@ -147,7 +162,7 @@ native DHookAddEntityListener(ListenType:type, ListenCB:callback); * * @return True if one was removed false otherwise. */ -native bool:DHookRemoveEntityListener(ListenType:type, ListenCB:callback); +native bool DHookRemoveEntityListener(ListenType type, ListenCB callback); /* Creates a hook * @@ -155,11 +170,11 @@ native bool:DHookRemoveEntityListener(ListenType:type, ListenCB:callback); * @param hooktype Type of hook * @param returntype Type type of return * @param thistype Type of this pointer or ignore (ignore can be used if not needed) - * @param callback Callback function + * @param callback Optional callback function, if not set here must be set when hooking. * * @return Returns setup handle for the hook or INVALID_HANDLE. */ -native Handle:DHookCreate(offset, HookType:hooktype, ReturnType:returntype, ThisPointerType:thisPointertype, DHookCallback:callback); +native Handle DHookCreate(int offset, HookType hooktype, ReturnType returntype, ThisPointerType thistype, DHookCallback callback=INVALID_FUNCTION); /* Adds param to a hook setup * @@ -171,8 +186,7 @@ native Handle:DHookCreate(offset, HookType:hooktype, ReturnType:returntype, This * @error Invalid setup handle or too many params added (request upping the max in thread) * @noreturn */ -native DHookAddParam(Handle:setup, HookParamType:type, size=-1, DHookPassFlag:flag=DHookPass_ByVal); -//native DHookAddParam(Handle:setup, HookParamType:type); +native void DHookAddParam(Handle setup, HookParamType type, int size=-1, DHookPassFlag flag=DHookPass_ByVal); /* Hook entity * @@ -180,22 +194,24 @@ native DHookAddParam(Handle:setup, HookParamType:type, size=-1, DHookPassFlag:fl * @param post True to make the hook a post hook. (If you need to change the retunr value or need the return value use a post hook! If you need to change params and return use a pre and post hook!) * @param entity Entity index to hook on. * @param removalcb Callback for when the hook is removed (Entity hooks are auto-removed on entity destroyed and will call this callback) + * @param callback Optional callback function, if not set here must be set when creating the hook. * - * @error Invalid setup handle, invalid entity or invalid hook type. + * @error Invalid setup handle, invalid address, invalid hook type or invalid callback. * @return -1 on fail a hookid on success */ -native DHookEntity(Handle:setup, bool:post, entity, DHookRemovalCB:removalcb=DHookRemovalCB:-1); +native int DHookEntity(Handle setup, bool post, int entity, DHookRemovalCB removalcb=INVALID_FUNCTION, DHookCallback callback=INVALID_FUNCTION); /* Hook gamerules * * @param setup Setup handle to use to add the hook. * @param post True to make the hook a post hook. (If you need to change the retunr value or need the return value use a post hook! If you need to change params and return use a pre and post hook!) * @param removalcb Callback for when the hook is removed (Game rules hooks are auto-removed on map end and will call this callback) + * @param callback Optional callback function, if not set here must be set when creating the hook. * - * @error Invalid setup handle, failing to get gamerules pointer or invalid hook type. + * @error Invalid setup handle, invalid address, invalid hook type or invalid callback. * @return -1 on fail a hookid on success */ -native DHookGamerules(Handle:setup, bool:post, DHookRemovalCB:removalcb=DHookRemovalCB:-1); +native int DHookGamerules(Handle setup, bool post, DHookRemovalCB removalcb=INVALID_FUNCTION, DHookCallback callback=INVALID_FUNCTION); /* Hook a raw pointer * @@ -203,11 +219,12 @@ native DHookGamerules(Handle:setup, bool:post, DHookRemovalCB:removalcb=DHookRem * @param post True to make the hook a post hook. (If you need to change the retunr value or need the return value use a post hook! If you need to change params and return use a pre and post hook!) * @param addr This pointer address. * @param removalcb Callback for when the hook is removed (Entity hooks are auto-removed on entity destroyed and will call this callback) + * @param callback Optional callback function, if not set here must be set when creating the hook. * - * @error Invalid setup handle, invalid address or invalid hook type. + * @error Invalid setup handle, invalid address, invalid hook type or invalid callback. * @return -1 on fail a hookid on success */ -native DHookRaw(Handle:setup, bool:post, Address:addr, DHookRemovalCB:removalcb=DHookRemovalCB:-1); +native int DHookRaw(Handle setup, bool post, Address addr, DHookRemovalCB removalcb=INVALID_FUNCTION, DHookCallback callback=INVALID_FUNCTION); /* Remove hook by hook id * @@ -216,7 +233,7 @@ native DHookRaw(Handle:setup, bool:post, Address:addr, DHookRemovalCB:removalcb= * @return true on success false otherwise * @note This will not fire the removal callback! */ -native bool:DHookRemoveHookID(hookid); +native bool DHookRemoveHookID(int hookid); /* Get param value (Only use for: int, entity, bool or float param types) * @@ -226,7 +243,7 @@ native bool:DHookRemoveHookID(hookid); * @error Invalid handle. Invalid param number. Invalid param type. * @return value if num greater than 0. If 0 returns paramcount. */ -native any:DHookGetParam(Handle:hParams, num); +native any DHookGetParam(Handle hParams, int num); /* Get vector param value * @@ -237,7 +254,7 @@ native any:DHookGetParam(Handle:hParams, num); * @error Invalid handle. Invalid param number. Invalid param type. * @noreturn */ -native DHookGetParamVector(Handle:hParams, num, Float:vec[3]); +native void DHookGetParamVector(Handle hParams, int num, float vec[3]); /* Get string param value * @@ -247,9 +264,9 @@ native DHookGetParamVector(Handle:hParams, num, Float:vec[3]); * @param size Buffer size * * @error Invalid handle. Invalid param number. Invalid param type. - * @return value if num greater than 0. + * @noreturn */ -native DHookGetParamString(Handle:hParams, num, String:buffer[], size); +native void DHookGetParamString(Handle hParams, int num, char[] buffer, int size); /* Set param value (Only use for: int, entity, bool or float param types) * @@ -260,7 +277,7 @@ native DHookGetParamString(Handle:hParams, num, String:buffer[], size); * @error Invalid handle. Invalid param number. Invalid param type. * @noreturn */ -native DHookSetParam(Handle:hParams, num, any:value); +native void DHookSetParam(Handle hParams, int num, any value); /* Set vector param value * @@ -271,7 +288,7 @@ native DHookSetParam(Handle:hParams, num, any:value); * @error Invalid handle. Invalid param number. Invalid param type. * @noreturn */ -native DHookSetParamVector(Handle:hParams, num, Float:vec[3]); +native void DHookSetParamVector(Handle hParams, int num, float vec[3]); /* Set string param value * @@ -282,7 +299,7 @@ native DHookSetParamVector(Handle:hParams, num, Float:vec[3]); * @error Invalid handle. Invalid param number. Invalid param type. * @noreturn */ -native DHookSetParamString(Handle:hParams, num, String:value[]); +native void DHookSetParamString(Handle hParams, int num, char[] value); /* Get return value (Only use for: int, entity, bool or float return types) * @@ -291,7 +308,7 @@ native DHookSetParamString(Handle:hParams, num, String:value[]); * @error Invalid Handle, invalid type. * @return Returns default value if prehook returns actual value if post hook. */ -native any:DHookGetReturn(Handle:hReturn); +native any DHookGetReturn(Handle hReturn); /* Get return vector value * @@ -301,7 +318,7 @@ native any:DHookGetReturn(Handle:hReturn); * @error Invalid Handle, invalid type. * @noreturn */ -native DHookGetReturnVector(Handle:hReturn, Float:vec[3]); +native void DHookGetReturnVector(Handle hReturn, float vec[3]); /* Get return string value * @@ -312,7 +329,7 @@ native DHookGetReturnVector(Handle:hReturn, Float:vec[3]); * @error Invalid Handle, invalid type. * @noreturn */ -native DHookGetReturnString(Handle:hReturn, String:buffer[], size); +native void DHookGetReturnString(Handle hReturn, char[] buffer, int size); /* Set return value (Only use for: int, entity, bool or float return types) * @@ -322,7 +339,7 @@ native DHookGetReturnString(Handle:hReturn, String:buffer[], size); * @error Invalid Handle, invalid type. * @noreturn */ -native DHookSetReturn(Handle:hReturn, any:value); +native void DHookSetReturn(Handle hReturn, any value); /* Set return vector value * @@ -332,7 +349,7 @@ native DHookSetReturn(Handle:hReturn, any:value); * @error Invalid Handle, invalid type. * @noreturn */ -native DHookSetReturnVector(Handle:hReturn, Float:vec[3]); +native void DHookSetReturnVector(Handle hReturn, float vec[3]); /* Set return string value * @@ -342,24 +359,26 @@ native DHookSetReturnVector(Handle:hReturn, Float:vec[3]); * @error Invalid Handle, invalid type. * @noreturn */ -native DHookSetReturnString(Handle:hReturn, String:value[]); +native void DHookSetReturnString(Handle hReturn, char[] value); + +//WE SHOULD WRAP THESE AROUND STOCKS FOR NON PTR AS WE SUPPORT BOTH WITH THESE NATIVE'S /* Gets an objects variable value * * @param hParams Handle to params structure - * @param num Param number to get. (Example if the function has 2 params and you need the value of the first param num would be 1. 0 Will return the number of params stored) + * @param num Param number to get. * @param offset Offset within the object to the var to get. * @param type Type of var it is * * @error Invalid handle. Invalid param number. Invalid param type. Invalid Object type. * @return Value of the objects var. If EHANDLE type or entity returns entity index. */ -native any:DHookGetParamObjectPtrVar(Handle:hParams, num, offset, ObjectValueType:type); +native any DHookGetParamObjectPtrVar(Handle hParams, int num, int offset, ObjectValueType type); /* Sets an objects variable value * * @param hParams Handle to params structure - * @param num Param number to set. (Example if the function has 2 params and you need the value of the first param num would be 1. 0 Will return the number of params stored) + * @param num Param number to set. * @param offset Offset within the object to the var to set. * @param type Type of var it is * @param value The value to set the var to. @@ -367,25 +386,25 @@ native any:DHookGetParamObjectPtrVar(Handle:hParams, num, offset, ObjectValueTyp * @error Invalid handle. Invalid param number. Invalid param type. Invalid Object type. * @noreturn */ -native DHookSetParamObjectPtrVar(Handle:hParams, num, offset, ObjectValueType:type, any:value); +native void DHookSetParamObjectPtrVar(Handle hParams, int num, int offset, ObjectValueType type, any value); /* Gets an objects vector variable value * * @param hParams Handle to params structure - * @param num Param number to get. (Example if the function has 2 params and you need the value of the first param num would be 1. 0 Will return the number of params stored) + * @param num Param number to get. * @param offset Offset within the object to the var to get. * @param type Type of var it is * @param buffer Buffer to store the result vector * * @error Invalid handle. Invalid param number. Invalid param type. Invalid Object type. - * @return Value of the objects var. + * @noreturn */ -native DHookGetParamObjectPtrVarVector(Handle:hParams, num, offset, ObjectValueType:type, Float:buffer[3]); +native void DHookGetParamObjectPtrVarVector(Handle hParams, int num, int offset, ObjectValueType type, float buffer[3]); /* Sets an objects vector variable value * * @param hParams Handle to params structure - * @param num Param number to set. (Example if the function has 2 params and you need the value of the first param num would be 1. 0 Will return the number of params stored) + * @param num Param number to set. * @param offset Offset within the object to the var to set. * @param type Type of var it is * @param value The value to set the vector var to. @@ -393,12 +412,21 @@ native DHookGetParamObjectPtrVarVector(Handle:hParams, num, offset, ObjectValueT * @error Invalid handle. Invalid param number. Invalid param type. Invalid Object type. * @noreturn */ -native DHookSetParamObjectPtrVarVector(Handle:hParams, num, offset, ObjectValueType:type, Float:value[3]); +native void DHookSetParamObjectPtrVarVector(Handle hParams, int num, int offset, ObjectValueType type, float value[3]); - -//ADD DOCS OR ELSE -//WE SHOULD WRAP THESE AROUND STOCKS FOR NON PTR AS WE SUPPORT BOTH WITH THIS NATIVE -native DHookGetParamObjectPtrString(Handle:hParams, num, offset, ObjectValueType:type, String:buffer[], size); +/* Gets an objects string variable value + * + * @param hParams Handle to params structure + * @param num Param number to get. + * @param offset Offset within the object to the var to get. + * @param type Type of var it is + * @param buffer Buffer to store the result vector + * @param size Size of the buffer + * + * @error Invalid handle. Invalid param number. Invalid param type. Invalid Object type. + * @noreturn +*/ +native void DHookGetParamObjectPtrString(Handle hParams, int num, int offset, ObjectValueType type, char[] buffer, int size); /* Checks if a pointer param is null * @@ -408,9 +436,9 @@ native DHookGetParamObjectPtrString(Handle:hParams, num, offset, ObjectValueType * @error Non pointer param * @return True if null false otherwise. */ -native bool:DHookIsNullParam(Handle:hParams, num); +native bool DHookIsNullParam(Handle hParams, int num); -public Extension:__ext_dhooks = +public Extension __ext_dhooks = { name = "dhooks", file = "dhooks.ext", diff --git a/addons/sourcemod/scripting/include/sf2.inc b/addons/sourcemod/scripting/include/sf2.inc index b6467ad..3dc0e20 100644 --- a/addons/sourcemod/scripting/include/sf2.inc +++ b/addons/sourcemod/scripting/include/sf2.inc @@ -1,321 +1,325 @@ -#if defined _sf2_included - #endinput -#endif -#define _sf2_included - -// Some defines. -#define SF2_MAX_PROFILE_NAME_LENGTH 64 -#define SF2_MAX_NAME_LENGTH 32 - -#define MAX_BOSSES 32 -#define MAX_NODES -1 - -// Difficulty modifiers. -#define DIFFICULTY_EASY 0.75 -#define DIFFICULTY_NORMAL 1.0 -#define DIFFICULTY_HARD 2.0 -#define DIFFICULTY_INSANE 3.5 - -// Music system flags. -#define MUSICF_PAGES1PERCENT (1 << 0) -#define MUSICF_PAGES25PERCENT (1 << 1) -#define MUSICF_PAGES50PERCENT (1 << 2) -#define MUSICF_PAGES75PERCENT (1 << 3) -#define MUSICF_DEATH (1 << 4) -#define MUSICF_CHASE (1 << 5) -#define MUSICF_CHASEVISIBLE (1 << 6) -#define MUSICF_ALERT (1 << 7) -#define MUSICF_20DOLLARS (1 << 8) - -// Special round enumerations. -enum -{ - SPECIALROUND_DOUBLETROUBLE = 1, - SPECIALROUND_INSANEDIFFICULTY, - SPECIALROUND_LIGHTSOUT, - SPECIALROUND_MAXROUNDS -}; - -// Boss state enumerations. -enum -{ - STATE_IDLE = 0, - STATE_WANDER, - STATE_ALERT, - STATE_CHASE, - STATE_ATTACK, - STATE_STUN -}; - -enum SoundType -{ - SoundType_None = 0, - SoundType_Footstep, - SoundType_Voice, - SoundType_Weapon -}; - -enum -{ - Difficulty_Easy = 0, - Difficulty_Normal, - Difficulty_Hard, - Difficulty_Insane, - Difficulty_Max -}; - -enum -{ - Static_None = 0, - Static_Increase, - Static_Decrease -}; - -enum -{ - SF2BossType_Unknown = -1, - SF2BossType_Static = 0, - SF2BossType_Creeper, - SF2BossType_Chaser, - SF2BossType_AdvancedChaser, - SF2BossType_MaxTypes -}; - -enum SF2RoundState -{ - SF2RoundState_Invalid = -1, - SF2RoundState_Waiting = 0, // waiting for players - SF2RoundState_Intro, // if intro is enabled, intro stage for RED - SF2RoundState_Active, // round is running for RED - SF2RoundState_Escape, // escape stage for RED - SF2RoundState_Outro // round win for a team, next round coming soon -}; - -// Boss flags. -#define SFF_SPAWNONCE (1 << 0) -#define SFF_NOTELEPORT (1 << 1) -#define SFF_FAKE (1 << 2) -#define SFF_MARKEDASFAKE (1 << 3) -#define SFF_ATTACKWAITERS (1 << 4) -#define SFF_HASSTATICSHAKE (1 << 5) -#define SFF_STATICONLOOK (1 << 6) -#define SFF_STATICONRADIUS (1 << 7) -#define SFF_PROXIES (1 << 8) -#define SFF_WANDERMOVE (1 << 9) -#define SFF_HASJUMPSCARE (1 << 10) -#define SFF_HASSIGHTSOUNDS (1 << 11) -#define SFF_HASSTATICLOOPLOCALSOUND (1 << 12) -#define SFF_HASVIEWSHAKE (1 << 13) -#define SFF_COPIES (1 << 14) -#define SFF_ATTACKPROPS (1 << 15) - -// Interrup conditions. -#define COND_HEARDSUSPICIOUSSOUND (1 << 0) -#define COND_HEARDFOOTSTEP (1 << 1) -#define COND_HEARDFOOTSTEPLOUD (1 << 2) -#define COND_HEARDWEAPON (1 << 3) -#define COND_HEARDVOICE (1 << 4) -#define COND_CHASETARGETINVALIDATED (1 << 5) -#define COND_SAWENEMY (1 << 5) - - -forward SF2_OnBossAdded(iBossIndex); - -forward SF2_OnBossSpawn(iBossIndex); - -forward SF2_OnBossChangeState(iBossIndex, iOldState, iNewState); - -forward SF2_OnBossRemoved(iBossIndex); - -forward SF2_OnPagesSpawned(); - -forward SF2_OnClientBlinked(client); - -forward SF2_OnClientCaughtByBoss(client, iBossIndex); - -forward Action:SF2_OnClientGiveQueuePoints(client, &iAddAmount); - -forward SF2_OnClientActivateFlashlight(client); - -forward SF2_OnClientDeactivateFlashlight(client); - -forward SF2_OnClientBreakFlashlight(client); - -forward SF2_OnClientEscape(client); - -forward SF2_OnClientLooksAtBoss(client, iBossIndex); - -forward SF2_OnClientLooksAwayFromBoss(client, iBossIndex); - -forward SF2_OnClientStartDeathCam(client, iBossIndex); - -forward SF2_OnClientEndDeathCam(client, iBossIndex); - -forward Action:SF2_OnClientGetDefaultWalkSpeed(client, &Float:flDefault); - -forward Action:SF2_OnClientGetDefaultSprintSpeed(client, &Float:flDefault); - -forward Action:SF2_OnGroupGiveQueuePoints(iGroupIndex, &iAddAmount); - -forward SF2_OnClientDamagedByBoss(client, iBossIndex, inflictor, Float:flDamage, iDamageType); - -forward SF2_OnClientSpawnedAsProxy(client); - - -/** - * Returns a bool about the gamemode's state. - * - * @return True if the gamemode is running, false if not. - */ -native bool:SF2_IsRunning(); - -/** - * Returns the current difficulty of the round. - * - * @return Integer of the difficulty. - */ -native SF2_GetCurrentDifficulty(); - -/** - * Returns the current difficulty of the round. - * - * @param iDifficulty Difficulty number. - * @return Modifier float value of the indicated difficulty number. - */ -native Float:SF2_GetDifficultyModifier(iDifficulty); - -/** - * Returns a bool indicating whether or not a special round is currently running. - * - * @return True if a special round is running, false if not. - */ -native bool:SF2_IsSpecialRoundRunning(); - -/** - * Returns the type of special round that is running. - * - * @return Special round type. - */ -native SF2_GetSpecialRoundType(); - -/** - * Returns a bool about the client's elimination state. - * - * @param client Client index. - * @return True if the player is eliminated, false if not. - */ -native bool:SF2_IsClientEliminated(client); - -/** - * Returns a bool about the client's ghost mode state. - * - * @param client Client index. - * @return True if the player is in Ghost Mode, false if not. - */ -native bool:SF2_IsClientInGhostMode(client); - -/** - * Returns a bool if the client is in a Player vs. Player zone or not. - * - * @param client Client index. - * @return True if the player is in a PvP zone, false if not. - */ -native bool:SF2_IsClientInPvP(client); - -/** - * Tells whether if the client is a Proxy or not. - * - * @param client Client index. - * @return True if the player is a Proxy, false if not. - */ -native bool:SF2_IsClientProxy(client); - -/** - * Tells whether or not the client is looking at the boss. - * - * @param client Client index. - * @param iBossIndex Boss index. - * @return True if the player is a Proxy, false if not. - */ -native bool:SF2_IsClientLookingAtBoss(client, iBossIndex); - -/** - * Gives the amount of times the client has blinked in one life. This count will reset upon spawn. - * - * @param client Client index. - * @return Number of times the client has blinked in one life. - */ -native SF2_GetClientBlinkCount(client); - -/** - * If the client is a Proxy, then this returns the boss index that the client is associated with. - * - * @param client Client index. - * @return If the client is a proxy, then this will return a boss index, -1 if not. - */ -native SF2_GetClientProxyMaster(client); - -/** - * If the client is a Proxy, then this returns the amount of Control points the client has left. - * - * @param client Client index. - * @return If the client is a proxy, then this will return the amount of Control Points out of 100, else 0. - */ -native SF2_GetClientProxyControlAmount(client); - -/** - * If the client is a Proxy, then this returns the rate which each Control point will drain for the client. - * - * @param client Client index. - * @return If the client is a proxy, then this will return a boss index, -1 if not. - */ -native Float:SF2_GetClientProxyControlRate(client); - -native SF2_SetClientProxyMaster(client, iBossIndex); - -native SF2_SetClientProxyControlAmount(client, iAmount); - -native SF2_SetClientProxyControlRate(client, Float:flAmount); - -native SF2_GetMaxBossCount(); - -native SF2_EntIndexToBossIndex(iEntIndex); - -native SF2_BossIndexToEntIndex(iBossIndex); - -native SF2_BossIDToBossIndex(iBossID); - -native SF2_BossIndexToBossID(iBossID); - -native SF2_GetBossName(iBossIndex, String:sBuffer[], iBufferLen); - -native SF2_GetBossModelEntity(iBossIndex); - -native SF2_GetBossTarget(iBossIndex); - -native SF2_GetBossMaster(iBossIndex); - -native SF2_GetBossState(iBossIndex); - -native bool:SF2_IsBossProfileValid(const String:sProfile[]); - -native SF2_GetBossProfileNum(const String:sProfile[], const String:sKey[], iDefaultValue=0); - -native Float:SF2_GetBossProfileFloat(const String:sProfile[], const String:sKey[], Float:flDefaultValue=0.0); - -native bool:SF2_GetBossProfileString(const String:sProfile[], const String:sKey[], String:sBuffer[], iBufferLen, const String:sDefaultValue[]=""); - -native bool:SF2_GetBossProfileVector(const String:sProfile[], const String:sKey[], Float:flBuffer[3], const Float:flDefaultValue[3]=NULL_VECTOR); - -native bool:SF2_GetRandomStringFromBossProfile(const String:sProfile, const String:sKey[], String:sBuffer[], iBufferLen, iIndex=-1); - -public SharedPlugin:__pl_sf2 = -{ - name = "sf2", - file = "sf2.smx", -#if defined REQUIRE_PLUGIN - required = 1, -#else - required = 0, -#endif +#if defined _sf2_included + #endinput +#endif +#define _sf2_included + +// Some defines. +#define SF2_MAX_PROFILE_NAME_LENGTH 64 +#define SF2_MAX_NAME_LENGTH 32 + +#define MAX_BOSSES 32 +#define MAX_NODES -1 + +// Difficulty modifiers. +#define DIFFICULTY_EASY 0.75 +#define DIFFICULTY_NORMAL 1.0 +#define DIFFICULTY_HARD 2.0 +#define DIFFICULTY_INSANE 3.5 + +// Music system flags. +#define MUSICF_PAGES1PERCENT (1 << 0) +#define MUSICF_PAGES25PERCENT (1 << 1) +#define MUSICF_PAGES50PERCENT (1 << 2) +#define MUSICF_PAGES75PERCENT (1 << 3) +#define MUSICF_DEATH (1 << 4) +#define MUSICF_CHASE (1 << 5) +#define MUSICF_CHASEVISIBLE (1 << 6) +#define MUSICF_ALERT (1 << 7) +#define MUSICF_20DOLLARS (1 << 8) + +// Special round enumerations. +enum +{ + SPECIALROUND_DOUBLETROUBLE = 1, + SPECIALROUND_INSANEDIFFICULTY, + SPECIALROUND_SINGLEPLAYER, + SPECIALROUND_DOUBLEMAXPLAYERS, + SPECIALROUND_LIGHTSOUT, + SPECIALROUND_MAXROUNDS +}; + +// Boss state enumerations. +enum +{ + STATE_IDLE = 0, + STATE_WANDER, + STATE_ALERT, + STATE_CHASE, + STATE_ATTACK, + STATE_STUN +}; + +enum SoundType +{ + SoundType_None = 0, + SoundType_Footstep, + SoundType_Voice, + SoundType_Weapon +}; + +enum +{ + Difficulty_Easy = 0, + Difficulty_Normal, + Difficulty_Hard, + Difficulty_Insane, + Difficulty_Max +}; + +enum +{ + Static_None = 0, + Static_Increase, + Static_Decrease +}; + +enum +{ + SF2BossType_Unknown = -1, + SF2BossType_Static = 0, + SF2BossType_Creeper, + SF2BossType_Chaser, + SF2BossType_AdvancedChaser, + SF2BossType_MaxTypes +}; + +enum SF2RoundState +{ + SF2RoundState_Invalid = -1, + SF2RoundState_Waiting = 0, // waiting for players + SF2RoundState_Intro, // if intro is enabled, intro stage for RED + SF2RoundState_Active, // round is running for RED + SF2RoundState_Escape, // escape stage for RED + SF2RoundState_Outro // round win for a team, next round coming soon +}; + +// Boss flags. +#define SFF_SPAWNONCE (1 << 0) +#define SFF_NOTELEPORT (1 << 1) +#define SFF_FAKE (1 << 2) +#define SFF_MARKEDASFAKE (1 << 3) +#define SFF_ATTACKWAITERS (1 << 4) +#define SFF_HASSTATICSHAKE (1 << 5) +#define SFF_STATICONLOOK (1 << 6) +#define SFF_STATICONRADIUS (1 << 7) +#define SFF_PROXIES (1 << 8) +#define SFF_WANDERMOVE (1 << 9) +#define SFF_HASJUMPSCARE (1 << 10) +#define SFF_HASSIGHTSOUNDS (1 << 11) +#define SFF_HASSTATICLOOPLOCALSOUND (1 << 12) +#define SFF_HASVIEWSHAKE (1 << 13) +#define SFF_COPIES (1 << 14) +#define SFF_ATTACKPROPS (1 << 15) + +// Interrup conditions. +#define COND_HEARDSUSPICIOUSSOUND (1 << 0) +#define COND_HEARDFOOTSTEP (1 << 1) +#define COND_HEARDFOOTSTEPLOUD (1 << 2) +#define COND_HEARDWEAPON (1 << 3) +#define COND_HEARDVOICE (1 << 4) +#define COND_CHASETARGETINVALIDATED (1 << 5) +#define COND_SAWENEMY (1 << 5) + + +forward SF2_OnBossAdded(iBossIndex); + +forward SF2_OnBossSpawn(iBossIndex); + +forward SF2_OnBossChangeState(iBossIndex, iOldState, iNewState); + +forward SF2_OnBossRemoved(iBossIndex); + +forward SF2_OnPagesSpawned(); + +forward SF2_OnClientBlinked(client); + +forward SF2_OnClientCaughtByBoss(client, iBossIndex); + +forward Action:SF2_OnClientGiveQueuePoints(client, &iAddAmount); + +forward SF2_OnClientActivateFlashlight(client); + +forward SF2_OnClientDeactivateFlashlight(client); + +forward SF2_OnClientBreakFlashlight(client); + +forward SF2_OnClientEscape(client); + +forward SF2_OnClientLooksAtBoss(client, iBossIndex); + +forward SF2_OnClientLooksAwayFromBoss(client, iBossIndex); + +forward SF2_OnClientStartDeathCam(client, iBossIndex); + +forward SF2_OnClientEndDeathCam(client, iBossIndex); + +forward Action:SF2_OnClientGetDefaultWalkSpeed(client, &Float:flDefault); + +forward Action:SF2_OnClientGetDefaultSprintSpeed(client, &Float:flDefault); + +forward Action:SF2_OnGroupGiveQueuePoints(iGroupIndex, &iAddAmount); + +forward SF2_OnClientDamagedByBoss(client, iBossIndex, inflictor, Float:flDamage, iDamageType); + +forward SF2_OnClientSpawnedAsProxy(client); + + +/** + * Returns a bool about the gamemode's state. + * + * @return True if the gamemode is running, false if not. + */ +native bool:SF2_IsRunning(); + +/** + * Returns the current difficulty of the round. + * + * @return Integer of the difficulty. + */ +native SF2_GetCurrentDifficulty(); + +/** + * Returns the current difficulty of the round. + * + * @param iDifficulty Difficulty number. + * @return Modifier float value of the indicated difficulty number. + */ +native Float:SF2_GetDifficultyModifier(iDifficulty); + +/** + * Returns a bool indicating whether or not a special round is currently running. + * + * @return True if a special round is running, false if not. + */ +native bool:SF2_IsSpecialRoundRunning(); + +/** + * Returns the type of special round that is running. + * + * @return Special round type. + */ +native SF2_GetSpecialRoundType(); + +/** + * Returns a bool about the client's elimination state. + * + * @param client Client index. + * @return True if the player is eliminated, false if not. + */ +native bool:SF2_IsClientEliminated(client); + +/** + * Returns a bool about the client's ghost mode state. + * + * @param client Client index. + * @return True if the player is in Ghost Mode, false if not. + */ +native bool:SF2_IsClientInGhostMode(client); + +/** + * Returns a bool if the client is in a Player vs. Player zone or not. + * + * @param client Client index. + * @return True if the player is in a PvP zone, false if not. + */ +native bool:SF2_IsClientInPvP(client); + +/** + * Tells whether if the client is a Proxy or not. + * + * @param client Client index. + * @return True if the player is a Proxy, false if not. + */ +native bool:SF2_IsClientProxy(client); + +/** + * Tells whether or not the client is looking at the boss. + * + * @param client Client index. + * @param iBossIndex Boss index. + * @return True if the player is a Proxy, false if not. + */ +native bool:SF2_IsClientLookingAtBoss(client, iBossIndex); + +/** + * Gives the amount of times the client has blinked in one life. This count will reset upon spawn. + * + * @param client Client index. + * @return Number of times the client has blinked in one life. + */ +native SF2_GetClientBlinkCount(client); + +/** + * If the client is a Proxy, then this returns the boss index that the client is associated with. + * + * @param client Client index. + * @return If the client is a proxy, then this will return a boss index, -1 if not. + */ +native SF2_GetClientProxyMaster(client); + +/** + * If the client is a Proxy, then this returns the amount of Control points the client has left. + * + * @param client Client index. + * @return If the client is a proxy, then this will return the amount of Control Points out of 100, else 0. + */ +native SF2_GetClientProxyControlAmount(client); + +/** + * If the client is a Proxy, then this returns the rate which each Control point will drain for the client. + * + * @param client Client index. + * @return If the client is a proxy, then this will return a boss index, -1 if not. + */ +native Float:SF2_GetClientProxyControlRate(client); + +native SF2_SetClientProxyMaster(client, iBossIndex); + +native SF2_SetClientProxyControlAmount(client, iAmount); + +native SF2_SetClientProxyControlRate(client, Float:flAmount); + +native SF2_CollectAsPage(pageEnt, client); + +native SF2_GetMaxBossCount(); + +native SF2_EntIndexToBossIndex(iEntIndex); + +native SF2_BossIndexToEntIndex(iBossIndex); + +native SF2_BossIDToBossIndex(iBossID); + +native SF2_BossIndexToBossID(iBossID); + +native SF2_GetBossName(iBossIndex, String:sBuffer[], iBufferLen); + +native SF2_GetBossModelEntity(iBossIndex); + +native SF2_GetBossTarget(iBossIndex); + +native SF2_GetBossMaster(iBossIndex); + +native SF2_GetBossState(iBossIndex); + +native bool:SF2_IsBossProfileValid(const String:sProfile[]); + +native SF2_GetBossProfileNum(const String:sProfile[], const String:sKey[], iDefaultValue=0); + +native Float:SF2_GetBossProfileFloat(const String:sProfile[], const String:sKey[], Float:flDefaultValue=0.0); + +native bool:SF2_GetBossProfileString(const String:sProfile[], const String:sKey[], String:sBuffer[], iBufferLen, const String:sDefaultValue[]=""); + +native bool:SF2_GetBossProfileVector(const String:sProfile[], const String:sKey[], Float:flBuffer[3], const Float:flDefaultValue[3]=NULL_VECTOR); + +native bool:SF2_GetRandomStringFromBossProfile(const String:sProfile, const String:sKey[], String:sBuffer[], iBufferLen, iIndex=-1); + +public SharedPlugin:__pl_sf2 = +{ + name = "sf2", + file = "sf2.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif }; \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index bbadb42..b26e668 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -1,6517 +1,6376 @@ -#include <sourcemod> -#include <sdktools> -#include <sdkhooks> -#include <clientprefs> -#include <steamtools> -#include <tf2items> -#include <dhooks> -#include <navmesh> - -#include <tf2> -#include <tf2_stocks> -#include <morecolors> -#include <sf2> - -#undef REQUIRE_PLUGIN -#include <adminmenu> -#tryinclude <store/store-tf2footprints> -#define REQUIRE_PLUGIN - -#define DEBUG - -// If compiling with SM 1.7+, uncomment to compile and use SF2 methodmaps. -//#define METHODMAPS - -#define PLUGIN_VERSION "0.2.5-git132" -#define PLUGIN_VERSION_DISPLAY "0.2.5" - -public Plugin:myinfo = -{ - name = "RYTP Horror (Slender Fortress edit by lexuzieel special for Penek-Gaming.Ru)", - author = "KitRifty", - description = "Based on the game Slender: The Eight Pages.", - version = PLUGIN_VERSION, - url = "http://steamcommunity.com/groups/SlenderFortress" -} - -#define FILE_RESTRICTEDWEAPONS "configs/sf2/restrictedweapons.cfg" - -#define BOSS_THINKRATE 0.1 // doesn't really matter much since timers go at a minimum of 0.1 seconds anyways - -#define CRIT_SOUND "player/crit_hit.wav" -#define CRIT_PARTICLENAME "crit_text" - -#define PAGE_MODEL "models/rytp/horror/props/hint_paper.mdl" -#define PAGE_MODELSCALE 1.1 - -#define FLASHLIGHT_CLICKSOUND "rytp_horror/toggleflashlight.wav" -#define FLASHLIGHT_BREAKSOUND "ambient/energy/spark6.wav" -#define FLASHLIGHT_NOSOUND "player/suit_denydevice.wav" -#define PAGE_GRABSOUND "rytp_horror/grabpage_sound.wav" - -#define MUSIC_CHAN SNDCHAN_AUTO - -#define MUSIC_GOTPAGES1_SOUND "rytp_horror/grabpage_music_1.wav" -#define MUSIC_GOTPAGES2_SOUND "rytp_horror/grabpage_music_2.wav" -#define MUSIC_GOTPAGES3_SOUND "rytp_horror/grabpage_music_3.wav" -#define MUSIC_GOTPAGES4_SOUND "rytp_horror/grabpage_music_4.wav" -#define MUSIC_PAGE_VOLUME 1.0 - -#define SF2_INTRO_DEFAULT_MUSIC "rytp_horror/intro_music.mp3" - -#define SF2_HUD_TEXT_COLOR_R 127 -#define SF2_HUD_TEXT_COLOR_G 167 -#define SF2_HUD_TEXT_COLOR_B 141 -#define SF2_HUD_TEXT_COLOR_A 255 - -enum MuteMode -{ - MuteMode_Normal = 0, - MuteMode_DontHearOtherTeam, - MuteMode_DontHearOtherTeamIfNotProxy -}; - -// Offsets. -new g_offsPlayerFOV = -1; -new g_offsPlayerDefaultFOV = -1; -new g_offsPlayerFogCtrl = -1; -new g_offsPlayerPunchAngle = -1; -new g_offsPlayerPunchAngleVel = -1; -new g_offsFogCtrlEnable = -1; -new g_offsFogCtrlEnd = -1; - -new g_iParticleCriticalHit = -1; - -new bool:g_bEnabled; - -new Handle:g_hConfig; -new Handle:g_hRestrictedWeaponsConfig; -new Handle:g_hSpecialRoundsConfig; - -new Handle:g_hPageMusicRanges; - -new g_iSlenderModel[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new g_iSlenderPoseEnt[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new g_iSlenderCopyMaster[MAX_BOSSES] = { -1, ... }; -new Float:g_flSlenderEyePosOffset[MAX_BOSSES][3]; -new Float:g_flSlenderEyeAngOffset[MAX_BOSSES][3]; -new Float:g_flSlenderDetectMins[MAX_BOSSES][3]; -new Float:g_flSlenderDetectMaxs[MAX_BOSSES][3]; -new Handle:g_hSlenderThink[MAX_BOSSES]; -new Handle:g_hSlenderEntityThink[MAX_BOSSES]; -new Handle:g_hSlenderFakeTimer[MAX_BOSSES]; -new Float:g_flSlenderLastKill[MAX_BOSSES]; -new g_iSlenderState[MAX_BOSSES]; -new g_iSlenderTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new Float:g_flSlenderAcceleration[MAX_BOSSES]; -new Float:g_flSlenderGoalPos[MAX_BOSSES][3]; -new Float:g_flSlenderStaticRadius[MAX_BOSSES]; -new Float:g_flSlenderChaseDeathPosition[MAX_BOSSES][3]; -new bool:g_bSlenderChaseDeathPosition[MAX_BOSSES]; -new Float:g_flSlenderIdleAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderWalkAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderRunAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderJumpSpeed[MAX_BOSSES]; -new Float:g_flSlenderPathNodeTolerance[MAX_BOSSES]; -new Float:g_flSlenderPathNodeLookAhead[MAX_BOSSES]; -new bool:g_bSlenderFeelerReflexAdjustment[MAX_BOSSES]; -new Float:g_flSlenderFeelerReflexAdjustmentPos[MAX_BOSSES][3]; - -new g_iSlenderTeleportTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; - -new Float:g_flSlenderNextTeleportTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportTargetTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMinRange[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxRange[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxTargetTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxTargetStress[MAX_BOSSES] = { 0.0, ... }; -new Float:g_flSlenderTeleportPlayersRestTime[MAX_BOSSES][MAXPLAYERS + 1]; - -// For boss type 2 -// General variables -new g_iSlenderHealth[MAX_BOSSES]; -new Handle:g_hSlenderPath[MAX_BOSSES]; -new g_iSlenderCurrentPathNode[MAX_BOSSES] = { -1, ... }; -new bool:g_bSlenderAttacking[MAX_BOSSES]; -new Handle:g_hSlenderAttackTimer[MAX_BOSSES]; -new Float:g_flSlenderNextJump[MAX_BOSSES] = { -1.0, ... }; -new g_iSlenderInterruptConditions[MAX_BOSSES]; -new Float:g_flSlenderLastFoundPlayer[MAX_BOSSES][MAXPLAYERS + 1]; -new Float:g_flSlenderLastFoundPlayerPos[MAX_BOSSES][MAXPLAYERS + 1][3]; -new Float:g_flSlenderNextPathTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderCalculatedWalkSpeed[MAX_BOSSES]; -new Float:g_flSlenderCalculatedSpeed[MAX_BOSSES]; -new Float:g_flSlenderTimeUntilNoPersistence[MAX_BOSSES]; - -new Float:g_flSlenderProxyTeleportMinRange[MAX_BOSSES]; -new Float:g_flSlenderProxyTeleportMaxRange[MAX_BOSSES]; - -// Sound variables -new Float:g_flSlenderTargetSoundLastTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTargetSoundMasterPos[MAX_BOSSES][3]; // to determine hearing focus -new Float:g_flSlenderTargetSoundTempPos[MAX_BOSSES][3]; -new Float:g_flSlenderTargetSoundDiscardMasterPosTime[MAX_BOSSES]; -new bool:g_bSlenderInvestigatingSound[MAX_BOSSES]; -new SoundType:g_iSlenderTargetSoundType[MAX_BOSSES] = { SoundType_None, ... }; -new g_iSlenderTargetSoundCount[MAX_BOSSES]; -new Float:g_flSlenderLastHeardVoice[MAX_BOSSES]; -new Float:g_flSlenderLastHeardFootstep[MAX_BOSSES]; -new Float:g_flSlenderLastHeardWeapon[MAX_BOSSES]; - - -new Float:g_flSlenderNextJumpScare[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextVoiceSound[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextMoanSound[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextWanderPos[MAX_BOSSES] = { -1.0, ... }; - - -new Float:g_flSlenderTimeUntilRecover[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilAlert[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilIdle[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilChase[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilKill[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilNextProxy[MAX_BOSSES] = { -1.0, ... }; - -// Page data. -new g_iPageCount; -new g_iPageMax; -new Float:g_flPageFoundLastTime; -new bool:g_bPageRef; -new String:g_strPageRefModel[PLATFORM_MAX_PATH]; -new Float:g_flPageRefModelScale; - -static Handle:g_hPlayerIntroMusicTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Seeing Mr. Slendy data. -new bool:g_bPlayerSeesSlender[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerSeesSlenderLastTime[MAXPLAYERS + 1][MAX_BOSSES]; - -new Float:g_flPlayerSightSoundNextTime[MAXPLAYERS + 1][MAX_BOSSES]; - -new Float:g_flPlayerScareLastTime[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerScareNextTime[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerStaticAmount[MAXPLAYERS + 1]; - -new Float:g_flPlayerLastChaseBossEncounterTime[MAXPLAYERS + 1][MAX_BOSSES]; - -// Player static data. -new g_iPlayerStaticMode[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerStaticIncreaseRate[MAXPLAYERS + 1]; -new Float:g_flPlayerStaticDecreaseRate[MAXPLAYERS + 1]; -new Handle:g_hPlayerStaticTimer[MAXPLAYERS + 1]; -new g_iPlayerStaticMaster[MAXPLAYERS + 1] = { -1, ... }; -new String:g_strPlayerStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new String:g_strPlayerLastStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerLastStaticTime[MAXPLAYERS + 1]; -new Float:g_flPlayerLastStaticVolume[MAXPLAYERS + 1]; -new Handle:g_hPlayerLastStaticTimer[MAXPLAYERS + 1]; - -// Static shake data. -new g_iPlayerStaticShakeMaster[MAXPLAYERS + 1]; -new bool:g_bPlayerInStaticShake[MAXPLAYERS + 1]; -new String:g_strPlayerStaticShakeSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerStaticShakeMinVolume[MAXPLAYERS + 1]; -new Float:g_flPlayerStaticShakeMaxVolume[MAXPLAYERS + 1]; - -// Fake lag compensation for FF. -new bool:g_bPlayerLagCompensation[MAXPLAYERS + 1]; -new g_iPlayerLagCompensationTeam[MAXPLAYERS + 1]; - -// Hint data. -enum -{ - PlayerHint_Sprint = 0, - PlayerHint_Flashlight, - PlayerHint_MainMenu, - PlayerHint_Blink, - PlayerHint_MaxNum -}; - -enum PlayerPreferences -{ - bool:PlayerPreference_PvPAutoSpawn, - MuteMode:PlayerPreference_MuteMode, - bool:PlayerPreference_ShowHints, - bool:PlayerPreference_EnableProxySelection, - bool:PlayerPreference_ProjectedFlashlight -}; - -new bool:g_bPlayerHints[MAXPLAYERS + 1][PlayerHint_MaxNum]; -new g_iPlayerPreferences[MAXPLAYERS + 1][PlayerPreferences]; - -// Player data. -new g_iPlayerLastButtons[MAXPLAYERS + 1]; -new bool:g_bPlayerChoseTeam[MAXPLAYERS + 1]; -new bool:g_bPlayerEliminated[MAXPLAYERS + 1]; -new bool:g_bPlayerEscaped[MAXPLAYERS + 1]; -new g_iPlayerPageCount[MAXPLAYERS + 1]; -new g_iPlayerQueuePoints[MAXPLAYERS + 1]; -new bool:g_bPlayerPlaying[MAXPLAYERS + 1]; -new Handle:g_hPlayerOverlayCheck[MAXPLAYERS + 1]; - -new Handle:g_hPlayerSwitchBlueTimer[MAXPLAYERS + 1]; - -// Player stress data. -new Float:g_flPlayerStress[MAXPLAYERS + 1]; -new Float:g_flPlayerStressNextUpdateTime[MAXPLAYERS + 1]; - -// Proxy data. -new bool:g_bPlayerProxy[MAXPLAYERS + 1]; -new bool:g_bPlayerProxyAvailable[MAXPLAYERS + 1]; -new Handle:g_hPlayerProxyAvailableTimer[MAXPLAYERS + 1]; -new bool:g_bPlayerProxyAvailableInForce[MAXPLAYERS + 1]; -new g_iPlayerProxyAvailableCount[MAXPLAYERS + 1]; -new g_iPlayerProxyMaster[MAXPLAYERS + 1]; -new g_iPlayerProxyControl[MAXPLAYERS + 1]; -new Handle:g_hPlayerProxyControlTimer[MAXPLAYERS + 1]; -new Float:g_flPlayerProxyControlRate[MAXPLAYERS + 1]; -new Handle:g_flPlayerProxyVoiceTimer[MAXPLAYERS + 1]; -new g_iPlayerProxyAskMaster[MAXPLAYERS + 1] = { -1, ... }; -new Float:g_iPlayerProxyAskPosition[MAXPLAYERS + 1][3]; - -new g_iPlayerDesiredFOV[MAXPLAYERS + 1]; - -new Handle:g_hPlayerPostWeaponsTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Music system. -new g_iPlayerMusicFlags[MAXPLAYERS + 1]; -new String:g_strPlayerMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerMusicVolume[MAXPLAYERS + 1]; -new Float:g_flPlayerMusicTargetVolume[MAXPLAYERS + 1]; -new Handle:g_hPlayerMusicTimer[MAXPLAYERS + 1]; -new g_iPlayerPageMusicMaster[MAXPLAYERS + 1]; - -// Chase music system, which apparently also uses the alert song system. And the idle sound system. -new String:g_strPlayerChaseMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new String:g_strPlayerChaseMusicSee[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerChaseMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerChaseMusicSeeVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerChaseMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerChaseMusicSeeTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayerChaseMusicMaster[MAXPLAYERS + 1] = { -1, ... }; -new g_iPlayerChaseMusicSeeMaster[MAXPLAYERS + 1] = { -1, ... }; - -new String:g_strPlayerAlertMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerAlertMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerAlertMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayerAlertMusicMaster[MAXPLAYERS + 1] = { -1, ... }; - -new String:g_strPlayer20DollarsMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayer20DollarsMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayer20DollarsMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayer20DollarsMusicMaster[MAXPLAYERS + 1] = { -1, ... }; - -// Player overlay data -new Handle:g_hOverlayUpdateTimer[MAXPLAYERS + 1]; - - -new SF2RoundState:g_iRoundState = SF2RoundState_Invalid; -new bool:g_bRoundGrace = false; -new Float:g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; -new bool:g_bRoundInfiniteFlashlight = false; -new bool:g_bRoundInfiniteBlink = false; -new bool:g_bRoundInfiniteSprint = false; - -static Handle:g_hRoundGraceTimer = INVALID_HANDLE; -static Handle:g_hRoundTimer = INVALID_HANDLE; -static Handle:g_hVoteTimer = INVALID_HANDLE; -static String:g_strRoundBossProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - -static g_iRoundCount = 0; -static g_iRoundEndCount = 0; -static g_iRoundActiveCount = 0; -static g_iRoundTime = 0; -static g_iRoundTimeLimit = 0; -static g_iRoundEscapeTimeLimit = 0; -static g_iRoundTimeGainFromPage = 0; -static bool:g_bRoundHasEscapeObjective = false; - -static g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; - -static g_iRoundIntroFadeColor[4] = { 255, ... }; -static Float:g_flRoundIntroFadeHoldTime; -static Float:g_flRoundIntroFadeDuration; -static Handle:g_hRoundIntroTimer = INVALID_HANDLE; -static bool:g_bRoundIntroTextDefault = true; -static Handle:g_hRoundIntroTextTimer = INVALID_HANDLE; -static g_iRoundIntroText; -static String:g_strRoundIntroMusic[PLATFORM_MAX_PATH] = ""; - -static g_iRoundWarmupRoundCount = 0; - -static bool:g_bRoundWaitingForPlayers = false; - -// Special round variables. -new bool:g_bSpecialRound = false; -new g_iSpecialRoundType = 0; -new bool:g_bSpecialRoundNew = false; -new bool:g_bSpecialRoundContinuous = false; -new g_iSpecialRoundCount = 1; -new bool:g_bPlayerPlayedSpecialRound[MAXPLAYERS + 1] = { true, ... }; - -// New boss round variables. -static bool:g_bNewBossRound = false; -static bool:g_bNewBossRoundNew = false; -static bool:g_bNewBossRoundContinuous = false; -static g_iNewBossRoundCount = 1; -static bool:g_bPlayerPlayedNewBossRound[MAXPLAYERS + 1] = { true, ... }; -static String:g_strNewBossRoundProfile[64] = ""; - -static Handle:g_hRoundMessagesTimer = INVALID_HANDLE; -static g_iRoundMessagesNum = 0; - -static Handle:g_hBossCountUpdateTimer = INVALID_HANDLE; -static Handle:g_hClientAverageUpdateTimer = INVALID_HANDLE; - -// Server variables. -new Handle:g_cvVersion; -new Handle:g_cvEnabled; -new Handle:g_cvSlenderMapsOnly; -new Handle:g_cvPlayerViewbobEnabled; -new Handle:g_cvPlayerShakeEnabled; -new Handle:g_cvPlayerShakeFrequencyMax; -new Handle:g_cvPlayerShakeAmplitudeMax; -new Handle:g_cvGraceTime; -new Handle:g_cvAllChat; -new Handle:g_cv20Dollars; -new Handle:g_cvMaxPlayers; -new Handle:g_cvMaxPlayersOverride; -new Handle:g_cvCampingEnabled; -new Handle:g_cvCampingMaxStrikes; -new Handle:g_cvCampingStrikesWarn; -new Handle:g_cvCampingMinDistance; -new Handle:g_cvCampingNoStrikeSanity; -new Handle:g_cvCampingNoStrikeBossDistance; -new Handle:g_cvDifficulty; -new Handle:g_cvBossMain; -new Handle:g_cvBossProfileOverride; -new Handle:g_cvPlayerBlinkRate; -new Handle:g_cvPlayerBlinkHoldTime; -new Handle:g_cvSpecialRoundBehavior; -new Handle:g_cvSpecialRoundForce; -new Handle:g_cvSpecialRoundOverride; -new Handle:g_cvSpecialRoundInterval; -new Handle:g_cvNewBossRoundBehavior; -new Handle:g_cvNewBossRoundInterval; -new Handle:g_cvNewBossRoundForce; -new Handle:g_cvPlayerVoiceDistance; -new Handle:g_cvPlayerVoiceWallScale; -new Handle:g_cvUltravisionEnabled; -new Handle:g_cvUltravisionRadiusRed; -new Handle:g_cvUltravisionRadiusBlue; -new Handle:g_cvUltravisionBrightness; -new Handle:g_cvGhostModeConnectionCheck; -new Handle:g_cvGhostModeConnectionTolerance; -new Handle:g_cvIntroEnabled; -new Handle:g_cvIntroDefaultHoldTime; -new Handle:g_cvIntroDefaultFadeTime; -new Handle:g_cvTimeLimit; -new Handle:g_cvTimeLimitEscape; -new Handle:g_cvTimeGainFromPageGrab; -new Handle:g_cvWarmupRound; -new Handle:g_cvWarmupRoundNum; -new Handle:g_cvPlayerViewbobHurtEnabled; -new Handle:g_cvPlayerViewbobSprintEnabled; -new Handle:g_cvPlayerFakeLagCompensation; -new Handle:g_cvPlayerProxyWaitTime; -new Handle:g_cvPlayerProxyAsk; -new Handle:g_cvHalfZatoichiHealthGain; -new Handle:g_cvBlockSuicideDuringRound; - -new Handle:g_cvPlayerInfiniteSprintOverride; -new Handle:g_cvPlayerInfiniteFlashlightOverride; -new Handle:g_cvPlayerInfiniteBlinkOverride; - -new Handle:g_cvGravity; -new Float:g_flGravity; - -new Handle:g_cvMaxRounds; - -new bool:g_b20Dollars; - -new bool:g_bPlayerShakeEnabled; -new bool:g_bPlayerViewbobEnabled; -new bool:g_bPlayerViewbobHurtEnabled; -new bool:g_bPlayerViewbobSprintEnabled; - -new Handle:g_hHudSync; -new Handle:g_hHudSync2; -new Handle:g_hRoundTimerSync; - -new Handle:g_hCookie; - -// Global forwards. -new Handle:fOnBossAdded; -new Handle:fOnBossSpawn; -new Handle:fOnBossChangeState; -new Handle:fOnBossRemoved; -new Handle:fOnPagesSpawned; -new Handle:fOnClientBlink; -new Handle:fOnClientCaughtByBoss; -new Handle:fOnClientGiveQueuePoints; -new Handle:fOnClientActivateFlashlight; -new Handle:fOnClientDeactivateFlashlight; -new Handle:fOnClientBreakFlashlight; -new Handle:fOnClientEscape; -new Handle:fOnClientLooksAtBoss; -new Handle:fOnClientLooksAwayFromBoss; -new Handle:fOnClientStartDeathCam; -new Handle:fOnClientEndDeathCam; -new Handle:fOnClientGetDefaultWalkSpeed; -new Handle:fOnClientGetDefaultSprintSpeed; -new Handle:fOnClientSpawnedAsProxy; -new Handle:fOnClientDamagedByBoss; -new Handle:fOnGroupGiveQueuePoints; - -new Handle:g_hSDKWeaponScattergun; -new Handle:g_hSDKWeaponPistolScout; -new Handle:g_hSDKWeaponBat; -new Handle:g_hSDKWeaponSniperRifle; -new Handle:g_hSDKWeaponSMG; -new Handle:g_hSDKWeaponKukri; -new Handle:g_hSDKWeaponRocketLauncher; -new Handle:g_hSDKWeaponShotgunSoldier; -new Handle:g_hSDKWeaponShovel; -new Handle:g_hSDKWeaponGrenadeLauncher; -new Handle:g_hSDKWeaponStickyLauncher; -new Handle:g_hSDKWeaponBottle; -new Handle:g_hSDKWeaponMinigun; -new Handle:g_hSDKWeaponShotgunHeavy; -new Handle:g_hSDKWeaponFists; -new Handle:g_hSDKWeaponSyringeGun; -new Handle:g_hSDKWeaponMedigun; -new Handle:g_hSDKWeaponBonesaw; -new Handle:g_hSDKWeaponFlamethrower; -new Handle:g_hSDKWeaponShotgunPyro; -new Handle:g_hSDKWeaponFireaxe; -new Handle:g_hSDKWeaponRevolver; -new Handle:g_hSDKWeaponKnife; -new Handle:g_hSDKWeaponInvis; -new Handle:g_hSDKWeaponShotgunPrimary; -new Handle:g_hSDKWeaponPistol; -new Handle:g_hSDKWeaponWrench; - -new Handle:g_hSDKGetMaxHealth; -new Handle:g_hSDKWantsLagCompensationOnEntity; -new Handle:g_hSDKShouldTransmit; - -#include "rytp_horror/stocks.sp" -#include "rytp_horror/overlay.sp" -#include "rytp_horror/logging.sp" -#include "rytp_horror/debug.sp" -#include "rytp_horror/profiles.sp" -#include "rytp_horror/nav.sp" -#include "rytp_horror/effects.sp" -#include "rytp_horror/playergroups.sp" -#include "rytp_horror/menus.sp" -#include "rytp_horror/pvp.sp" -#include "rytp_horror/client.sp" -#include "rytp_horror/npc.sp" -#include "rytp_horror/specialround.sp" -#include "rytp_horror/adminmenu.sp" - - -#define SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND "ui/item_acquired.wav" - -// ========================================================== -// GENERAL PLUGIN HOOK FUNCTIONS -// ========================================================== - -public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) -{ - RegPluginLibrary("sf2"); - - fOnBossAdded = CreateGlobalForward("SF2_OnBossAdded", ET_Ignore, Param_Cell); - fOnBossSpawn = CreateGlobalForward("SF2_OnBossSpawn", ET_Ignore, Param_Cell); - fOnBossChangeState = CreateGlobalForward("SF2_OnBossChangeState", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); - fOnBossRemoved = CreateGlobalForward("SF2_OnBossRemoved", ET_Ignore, Param_Cell); - fOnPagesSpawned = CreateGlobalForward("SF2_OnPagesSpawned", ET_Ignore); - fOnClientBlink = CreateGlobalForward("SF2_OnClientBlink", ET_Ignore, Param_Cell); - fOnClientCaughtByBoss = CreateGlobalForward("SF2_OnClientCaughtByBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientGiveQueuePoints = CreateGlobalForward("SF2_OnClientGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientActivateFlashlight = CreateGlobalForward("SF2_OnClientActivateFlashlight", ET_Ignore, Param_Cell); - fOnClientDeactivateFlashlight = CreateGlobalForward("SF2_OnClientDeactivateFlashlight", ET_Ignore, Param_Cell); - fOnClientBreakFlashlight = CreateGlobalForward("SF2_OnClientBreakFlashlight", ET_Ignore, Param_Cell); - fOnClientEscape = CreateGlobalForward("SF2_OnClientEscape", ET_Ignore, Param_Cell); - fOnClientLooksAtBoss = CreateGlobalForward("SF2_OnClientLooksAtBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientLooksAwayFromBoss = CreateGlobalForward("SF2_OnClientLooksAwayFromBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientStartDeathCam = CreateGlobalForward("SF2_OnClientStartDeathCam", ET_Ignore, Param_Cell, Param_Cell); - fOnClientEndDeathCam = CreateGlobalForward("SF2_OnClientEndDeathCam", ET_Ignore, Param_Cell, Param_Cell); - fOnClientGetDefaultWalkSpeed = CreateGlobalForward("SF2_OnClientGetDefaultWalkSpeed", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientGetDefaultSprintSpeed = CreateGlobalForward("SF2_OnClientGetDefaultSprintSpeed", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientSpawnedAsProxy = CreateGlobalForward("SF2_OnClientSpawnedAsProxy", ET_Ignore, Param_Cell); - fOnClientDamagedByBoss = CreateGlobalForward("SF2_OnClientDamagedByBoss", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell); - fOnGroupGiveQueuePoints = CreateGlobalForward("SF2_OnGroupGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); - - CreateNative("SF2_IsRunning", Native_IsRunning); - CreateNative("SF2_GetCurrentDifficulty", Native_GetCurrentDifficulty); - CreateNative("SF2_GetDifficultyModifier", Native_GetDifficultyModifier); - CreateNative("SF2_IsClientEliminated", Native_IsClientEliminated); - CreateNative("SF2_IsClientInGhostMode", Native_IsClientInGhostMode); - CreateNative("SF2_IsClientProxy", Native_IsClientProxy); - CreateNative("SF2_GetClientBlinkCount", Native_GetClientBlinkCount); - CreateNative("SF2_GetClientProxyMaster", Native_GetClientProxyMaster); - CreateNative("SF2_GetClientProxyControlAmount", Native_GetClientProxyControlAmount); - CreateNative("SF2_GetClientProxyControlRate", Native_GetClientProxyControlRate); - CreateNative("SF2_SetClientProxyMaster", Native_SetClientProxyMaster); - CreateNative("SF2_SetClientProxyControlAmount", Native_SetClientProxyControlAmount); - CreateNative("SF2_SetClientProxyControlRate", Native_SetClientProxyControlRate); - CreateNative("SF2_IsClientLookingAtBoss", Native_IsClientLookingAtBoss); - CreateNative("SF2_GetMaxBossCount", Native_GetMaxBosses); - CreateNative("SF2_EntIndexToBossIndex", Native_EntIndexToBossIndex); - CreateNative("SF2_BossIndexToEntIndex", Native_BossIndexToEntIndex); - CreateNative("SF2_BossIDToBossIndex", Native_BossIDToBossIndex); - CreateNative("SF2_BossIndexToBossID", Native_BossIndexToBossID); - CreateNative("SF2_GetBossName", Native_GetBossName); - CreateNative("SF2_GetBossModelEntity", Native_GetBossModelEntity); - CreateNative("SF2_GetBossTarget", Native_GetBossTarget); - CreateNative("SF2_GetBossMaster", Native_GetBossMaster); - CreateNative("SF2_GetBossState", Native_GetBossState); - CreateNative("SF2_IsBossProfileValid", Native_IsBossProfileValid); - CreateNative("SF2_GetBossProfileNum", Native_GetBossProfileNum); - CreateNative("SF2_GetBossProfileFloat", Native_GetBossProfileFloat); - CreateNative("SF2_GetBossProfileString", Native_GetBossProfileString); - CreateNative("SF2_GetBossProfileVector", Native_GetBossProfileVector); - CreateNative("SF2_GetRandomStringFromBossProfile", Native_GetRandomStringFromBossProfile); - - PvP_InitializeAPI(); - - SpecialRoundInitializeAPI(); - - return APLRes_Success; -} - -public OnPluginStart() -{ - LoadTranslations("core.phrases"); - LoadTranslations("common.phrases"); - LoadTranslations("sf2.phrases"); - - // Get offsets. - g_offsPlayerFOV = FindSendPropInfo("CBasePlayer", "m_iFOV"); - if (g_offsPlayerFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iFOV."); - - g_offsPlayerDefaultFOV = FindSendPropInfo("CBasePlayer", "m_iDefaultFOV"); - if (g_offsPlayerDefaultFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iDefaultFOV."); - - g_offsPlayerFogCtrl = FindSendPropInfo("CBasePlayer", "m_PlayerFog.m_hCtrl"); - if (g_offsPlayerFogCtrl == -1) LogError("Couldn't find CBasePlayer offset for m_PlayerFog.m_hCtrl!"); - - g_offsPlayerPunchAngle = FindSendPropInfo("CBasePlayer", "m_vecPunchAngle"); - if (g_offsPlayerPunchAngle == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngle!"); - - g_offsPlayerPunchAngleVel = FindSendPropInfo("CBasePlayer", "m_vecPunchAngleVel"); - if (g_offsPlayerPunchAngleVel == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngleVel!"); - - g_offsFogCtrlEnable = FindSendPropInfo("CFogController", "m_fog.enable"); - if (g_offsFogCtrlEnable == -1) LogError("Couldn't find CFogController offset for m_fog.enable!"); - - g_offsFogCtrlEnd = FindSendPropInfo("CFogController", "m_fog.end"); - if (g_offsFogCtrlEnd == -1) LogError("Couldn't find CFogController offset for m_fog.end!"); - - g_hPageMusicRanges = CreateArray(3); - - // Register console variables. - g_cvVersion = CreateConVar("sf2_version", PLUGIN_VERSION, "The current version of Slender Fortress. DO NOT TOUCH!", FCVAR_SPONLY | FCVAR_NOTIFY | FCVAR_DONTRECORD); - SetConVarString(g_cvVersion, PLUGIN_VERSION); - - g_cvEnabled = CreateConVar("sf2_enabled", "1", "Enable/Disable the Slender Fortress gamemode. This will take effect on map change.", FCVAR_NOTIFY | FCVAR_DONTRECORD); - g_cvSlenderMapsOnly = CreateConVar("sf2_slendermapsonly", "1", "Only enable the Slender Fortress gamemode on map names prefixed with \"slender_\" or \"sf2_\"."); - - g_cvGraceTime = CreateConVar("sf2_gracetime", "30.0"); - g_cvIntroEnabled = CreateConVar("sf2_intro_enabled", "1"); - g_cvIntroDefaultHoldTime = CreateConVar("sf2_intro_default_hold_time", "9.0"); - g_cvIntroDefaultFadeTime = CreateConVar("sf2_intro_default_fade_time", "1.0"); - - g_cvBlockSuicideDuringRound = CreateConVar("sf2_block_suicide_during_round", "0"); - - g_cvAllChat = CreateConVar("sf2_alltalk", "0"); - HookConVarChange(g_cvAllChat, OnConVarChanged); - - g_cvPlayerVoiceDistance = CreateConVar("sf2_player_voice_distance", "800.0", "The maximum distance RED can communicate in voice chat. Set to 0 if you want them to be heard at all times.", _, true, 0.0); - g_cvPlayerVoiceWallScale = CreateConVar("sf2_player_voice_scale_blocked", "0.5", "The distance required to hear RED in voice chat will be multiplied by this amount if something is blocking them."); - - g_cvPlayerViewbobEnabled = CreateConVar("sf2_player_viewbob_enabled", "1", "Enable/Disable player viewbobbing.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobEnabled, OnConVarChanged); - g_cvPlayerViewbobHurtEnabled = CreateConVar("sf2_player_viewbob_hurt_enabled", "0", "Enable/Disable player view tilting when hurt.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobHurtEnabled, OnConVarChanged); - g_cvPlayerViewbobSprintEnabled = CreateConVar("sf2_player_viewbob_sprint_enabled", "0", "Enable/Disable player step viewbobbing when sprinting.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobSprintEnabled, OnConVarChanged); - g_cvGravity = FindConVar("sv_gravity"); - HookConVarChange(g_cvGravity, OnConVarChanged); - - g_cvPlayerFakeLagCompensation = CreateConVar("sf2_player_fakelagcompensation", "0", "(EXPERIMENTAL) Enable/Disable fake lag compensation for some hitscan weapons such as the Sniper Rifle.", _, true, 0.0, true, 1.0); - - g_cvPlayerShakeEnabled = CreateConVar("sf2_player_shake_enabled", "1", "Enable/Disable player view shake during boss encounters.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerShakeEnabled, OnConVarChanged); - g_cvPlayerShakeFrequencyMax = CreateConVar("sf2_player_shake_frequency_max", "255", "Maximum frequency value of the shake. Should be a value between 1-255.", _, true, 1.0, true, 255.0); - g_cvPlayerShakeAmplitudeMax = CreateConVar("sf2_player_shake_amplitude_max", "5", "Maximum amplitude value of the shake. Should be a value between 1-16.", _, true, 1.0, true, 16.0); - - g_cvPlayerBlinkRate = CreateConVar("sf2_player_blink_rate", "0.33", "How long (in seconds) each bar on the player's Blink meter lasts.", _, true, 0.0); - g_cvPlayerBlinkHoldTime = CreateConVar("sf2_player_blink_holdtime", "0.15", "How long (in seconds) a player will stay in Blink mode when he or she blinks.", _, true, 0.0); - - g_cvUltravisionEnabled = CreateConVar("sf2_player_ultravision_enabled", "1", "Enable/Disable player Ultravision. This helps players see in the dark when their Flashlight is off or unavailable.", _, true, 0.0, true, 1.0); - g_cvUltravisionRadiusRed = CreateConVar("sf2_player_ultravision_radius_red", "512.0"); - g_cvUltravisionRadiusBlue = CreateConVar("sf2_player_ultravision_radius_blue", "800.0"); - g_cvUltravisionBrightness = CreateConVar("sf2_player_ultravision_brightness", "-4"); - - g_cvGhostModeConnectionCheck = CreateConVar("sf2_ghostmode_check_connection", "1", "Checks a player's connection while in Ghost Mode. If the check fails, the client is booted out of Ghost Mode and the action and client's SteamID is logged in the main SF2 log."); - g_cvGhostModeConnectionTolerance = CreateConVar("sf2_ghostmode_connection_tolerance", "2.5", "If sf2_ghostmode_check_connection is set to 1 and the client has timed out for at least this amount of time, the client will be booted out of Ghost Mode."); - - g_cv20Dollars = CreateConVar("sf2_20dollarmode", "0", "Enable/Disable $20 mode.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cv20Dollars, OnConVarChanged); - - g_cvMaxPlayers = CreateConVar("sf2_maxplayers", "5", "The maximum amount of players that can be in one round.", _, true, 1.0); - HookConVarChange(g_cvMaxPlayers, OnConVarChanged); - - g_cvMaxPlayersOverride = CreateConVar("sf2_maxplayers_override", "-1", "Overrides the maximum amount of players that can be in one round.", _, true, -1.0); - HookConVarChange(g_cvMaxPlayersOverride, OnConVarChanged); - - g_cvCampingEnabled = CreateConVar("sf2_anticamping_enabled", "1", "Enable/Disable anti-camping system for RED.", _, true, 0.0, true, 1.0); - g_cvCampingMaxStrikes = CreateConVar("sf2_anticamping_maxstrikes", "4", "How many 5-second intervals players are allowed to stay in one spot before he/she is forced to suicide.", _, true, 0.0); - g_cvCampingStrikesWarn = CreateConVar("sf2_anticamping_strikeswarn", "2", "The amount of strikes left where the player will be warned of camping."); - g_cvCampingMinDistance = CreateConVar("sf2_anticamping_mindistance", "128.0", "Every 5 seconds the player has to be at least this far away from his last position 5 seconds ago or else he'll get a strike."); - g_cvCampingNoStrikeSanity = CreateConVar("sf2_anticamping_no_strike_sanity", "0.1", "The camping system will NOT give any strikes under any circumstances if the players's Sanity is missing at least this much of his maximum Sanity (max is 1.0)."); - g_cvCampingNoStrikeBossDistance = CreateConVar("sf2_anticamping_no_strike_boss_distance", "512.0", "The camping system will NOT give any strikes under any circumstances if the player is this close to a boss (ignoring LOS)."); - g_cvBossMain = CreateConVar("sf2_boss_main", "slenderman", "The name of the main boss (its profile name, not its display name)"); - g_cvBossProfileOverride = CreateConVar("sf2_boss_profile_override", "", "Overrides which boss will be chosen next. Only applies to the first boss being chosen."); - g_cvDifficulty = CreateConVar("sf2_difficulty", "1", "Difficulty of the game. 1 = Normal, 2 = Hard, 3 = Insane.", _, true, 1.0, true, 3.0); - HookConVarChange(g_cvDifficulty, OnConVarChanged); - - g_cvSpecialRoundBehavior = CreateConVar("sf2_specialround_mode", "0", "0 = Special Round resets on next round, 1 = Special Round keeps going until all players have played (not counting spectators, recently joined players, and those who reset their queue points during the round)", _, true, 0.0, true, 1.0); - g_cvSpecialRoundForce = CreateConVar("sf2_specialround_forceenable", "-1", "Sets whether a Special Round will occur on the next round or not.", _, true, -1.0, true, 1.0); - g_cvSpecialRoundOverride = CreateConVar("sf2_specialround_forcetype", "-1", "Sets the type of Special Round that will be chosen on the next Special Round. Set to -1 to let the game choose.", _, true, -1.0); - g_cvSpecialRoundInterval = CreateConVar("sf2_specialround_interval", "5", "If this many rounds are completed, the next round will be a Special Round.", _, true, 0.0); - - g_cvNewBossRoundBehavior = CreateConVar("sf2_newbossround_mode", "0", "0 = boss selection will return to normal after the boss round, 1 = the new boss will continue being the boss until all players in the server have played against it (not counting spectators, recently joined players, and those who reset their queue points during the round).", _, true, 0.0, true, 1.0); - g_cvNewBossRoundInterval = CreateConVar("sf2_newbossround_interval", "3", "If this many rounds are completed, the next round's boss will be randomly chosen, but will not be the main boss.", _, true, 0.0); - g_cvNewBossRoundForce = CreateConVar("sf2_newbossround_forceenable", "-1", "Sets whether a new boss will be chosen on the next round or not. Set to -1 to let the game choose.", _, true, -1.0, true, 1.0); - - g_cvTimeLimit = CreateConVar("sf2_timelimit_default", "300", "The time limit of the round. Maps can change the time limit.", _, true, 0.0); - g_cvTimeLimitEscape = CreateConVar("sf2_timelimit_escape_default", "90", "The time limit to escape. Maps can change the time limit.", _, true, 0.0); - g_cvTimeGainFromPageGrab = CreateConVar("sf2_time_gain_page_grab", "12", "The time gained from grabbing a page. Maps can change the time gain amount."); - - g_cvWarmupRound = CreateConVar("sf2_warmupround", "1", "Enables/disables Warmup Rounds after the \"Waiting for Players\" phase.", _, true, 0.0, true, 1.0); - g_cvWarmupRoundNum = CreateConVar("sf2_warmupround_num", "1", "Sets the amount of Warmup Rounds that occur after the \"Waiting for Players\" phase.", _, true, 0.0); - - g_cvPlayerProxyWaitTime = CreateConVar("sf2_player_proxy_waittime", "35", "How long (in seconds) after a player was chosen to be a Proxy must the system wait before choosing him again."); - g_cvPlayerProxyAsk = CreateConVar("sf2_player_proxy_ask", "0", "Set to 1 if the player can choose before becoming a Proxy, set to 0 to force."); - - g_cvHalfZatoichiHealthGain = CreateConVar("sf2_halfzatoichi_healthgain", "20", "How much health should be gained from killing a player with the Half-Zatoichi? Set to -1 for default behavior."); - - g_cvPlayerInfiniteSprintOverride = CreateConVar("sf2_player_infinite_sprint_override", "-1", "1 = infinite sprint, 0 = never have infinite sprint, -1 = let the game choose.", _, true, -1.0, true, 1.0); - g_cvPlayerInfiniteFlashlightOverride = CreateConVar("sf2_player_infinite_flashlight_override", "-1", "1 = infinite flashlight, 0 = never have infinite flashlight, -1 = let the game choose.", _, true, -1.0, true, 1.0); - g_cvPlayerInfiniteBlinkOverride = CreateConVar("sf2_player_infinite_blink_override", "-1", "1 = infinite blink, 0 = never have infinite blink, -1 = let the game choose.", _, true, -1.0, true, 1.0); - - g_cvMaxRounds = FindConVar("mp_maxrounds"); - - g_hHudSync = CreateHudSynchronizer(); - g_hHudSync2 = CreateHudSynchronizer(); - g_hRoundTimerSync = CreateHudSynchronizer(); - g_hCookie = RegClientCookie("slender_cookie", "", CookieAccess_Private); - - // Register console commands. - RegConsoleCmd("sm_sf2", Command_MainMenu); - RegConsoleCmd("sm_slender", Command_MainMenu); - RegConsoleCmd("sm_horror", Command_MainMenu); - RegConsoleCmd("sm_slnext", Command_Next); - RegConsoleCmd("sm_slgroup", Command_Group); - RegConsoleCmd("sm_slgroupname", Command_GroupName); - RegConsoleCmd("sm_slghost", Command_GhostMode); - RegConsoleCmd("sm_slhelp", Command_Help); - RegConsoleCmd("sm_slsettings", Command_Settings); - RegConsoleCmd("sm_slcredits", Command_Credits); - RegConsoleCmd("sm_flashlight", Command_ToggleFlashlight); - RegConsoleCmd("+sprint", Command_SprintOn); - RegConsoleCmd("-sprint", Command_SprintOff); - - RegAdminCmd("sm_sf2_scare", Command_ClientPerformScare, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_spawn_boss", Command_SpawnSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_add_boss", Command_AddSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_add_boss_fake", Command_AddSlenderFake, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_remove_boss", Command_RemoveSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_getbossindexes", Command_GetBossIndexes, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_setplaystate", Command_ForceState, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_boss_attack_waiters", Command_SlenderAttackWaiters, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_boss_no_teleport", Command_SlenderNoTeleport, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_force_proxy", Command_ForceProxy, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_force_escape", Command_ForceEscape, ADMFLAG_CHEATS); - - // Hook onto existing console commands. - AddCommandListener(Hook_CommandBuild, "build"); - AddCommandListener(Hook_CommandSuicideAttempt, "kill"); - AddCommandListener(Hook_CommandSuicideAttempt, "explode"); - AddCommandListener(Hook_CommandSuicideAttempt, "joinclass"); - AddCommandListener(Hook_CommandSuicideAttempt, "join_class"); - AddCommandListener(Hook_CommandSuicideAttempt, "jointeam"); - AddCommandListener(Hook_CommandSuicideAttempt, "spectate"); - AddCommandListener(Hook_CommandVoiceMenu, "voicemenu"); - AddCommandListener(Hook_CommandSay, "say"); - - // Hook events. - HookEvent("teamplay_round_start", Event_RoundStart); - HookEvent("teamplay_round_win", Event_RoundEnd); - HookEvent("player_team", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("player_team", Event_PlayerTeam); - HookEvent("player_spawn", Event_PlayerSpawn); - HookEvent("player_hurt", Event_PlayerHurt); - HookEvent("post_inventory_application", Event_PostInventoryApplication); - HookEvent("item_found", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("teamplay_teambalanced_player", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("fish_notice", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("fish_notice__arm", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("player_death", Event_PlayerDeath); - - // Hook entities. - HookEntityOutput("trigger_multiple", "OnStartTouch", Hook_TriggerOnStartTouch); - HookEntityOutput("trigger_multiple", "OnEndTouch", Hook_TriggerOnEndTouch); - - // Hook usermessages. - HookUserMessage(GetUserMessageId("VoiceSubtitle"), Hook_BlockUserMessage, true); - - // Hook sounds. - AddNormalSoundHook(Hook_NormalSound); - - AddTempEntHook("Fire Bullets", Hook_TEFireBullets); - - InitializeBossProfiles(); - - NPCInitialize(); - - SetupMenus(); - - SetupAdminMenu(); - - SetupClassDefaultWeapons(); - - SetupPlayerGroups(); - - PvP_Initialize(); - - // @TODO: When cvars are finalized, set this to true. - AutoExecConfig(false); - -#if defined DEBUG - InitializeDebug(); -#endif -} - -public OnAllPluginsLoaded() -{ - SetupHooks(); -} - -public OnPluginEnd() -{ - for(new c = 1; c < MaxClients; c++) DestroySpriteOverlay(c); - StopPlugin(); -} - -static SetupHooks() -{ - // Check SDKHooks gamedata. - new Handle:hConfig = LoadGameConfigFile("sdkhooks.games"); - if (hConfig == INVALID_HANDLE) SetFailState("Couldn't find SDKHooks gamedata!"); - - StartPrepSDKCall(SDKCall_Entity); - PrepSDKCall_SetFromConf(hConfig, SDKConf_Virtual, "GetMaxHealth"); - PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); - if ((g_hSDKGetMaxHealth = EndPrepSDKCall()) == INVALID_HANDLE) - { - SetFailState("Failed to retrieve GetMaxHealth offset from SDKHooks gamedata!"); - } - - CloseHandle(hConfig); - - // Check our own gamedata. - hConfig = LoadGameConfigFile("sf2"); - if (hConfig == INVALID_HANDLE) SetFailState("Could not find SF2 gamedata!"); - - new iOffset = GameConfGetOffset(hConfig, "CTFPlayer::WantsLagCompensationOnEntity"); - g_hSDKWantsLagCompensationOnEntity = DHookCreate(iOffset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, Hook_ClientWantsLagCompensationOnEntity); - if (g_hSDKWantsLagCompensationOnEntity == INVALID_HANDLE) - { - SetFailState("Failed to create hook CTFPlayer::WantsLagCompensationOnEntity offset from SF2 gamedata!"); - } - - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_CBaseEntity); - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_ObjectPtr); - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_Unknown); - - iOffset = GameConfGetOffset(hConfig, "CBaseEntity::ShouldTransmit"); - g_hSDKShouldTransmit = DHookCreate(iOffset, HookType_Entity, ReturnType_Int, ThisPointer_CBaseEntity, Hook_EntityShouldTransmit); - if (g_hSDKShouldTransmit == INVALID_HANDLE) - { - SetFailState("Failed to create hook CBaseEntity::ShouldTransmit offset from SF2 gamedata!"); - } - - DHookAddParam(g_hSDKShouldTransmit, HookParamType_ObjectPtr); - - CloseHandle(hConfig); -} - -static SetupClassDefaultWeapons() -{ - // Scout - g_hSDKWeaponScattergun = PrepareItemHandle("tf_weapon_scattergun", 13, 0, 0, ""); - g_hSDKWeaponPistolScout = PrepareItemHandle("tf_weapon_pistol", 23, 0, 0, ""); - g_hSDKWeaponBat = PrepareItemHandle("tf_weapon_bat", 0, 0, 0, ""); - - // Sniper - g_hSDKWeaponSniperRifle = PrepareItemHandle("tf_weapon_sniperrifle", 14, 0, 0, ""); - g_hSDKWeaponSMG = PrepareItemHandle("tf_weapon_smg", 16, 0, 0, ""); - g_hSDKWeaponKukri = PrepareItemHandle("tf_weapon_club", 3, 0, 0, ""); - - // Soldier - g_hSDKWeaponRocketLauncher = PrepareItemHandle("tf_weapon_rocketlauncher", 18, 0, 0, ""); - g_hSDKWeaponShotgunSoldier = PrepareItemHandle("tf_weapon_shotgun", 10, 0, 0, ""); - g_hSDKWeaponShovel = PrepareItemHandle("tf_weapon_shovel", 6, 0, 0, ""); - - // Demoman - g_hSDKWeaponGrenadeLauncher = PrepareItemHandle("tf_weapon_grenadelauncher", 19, 0, 0, ""); - g_hSDKWeaponStickyLauncher = PrepareItemHandle("tf_weapon_pipebomblauncher", 20, 0, 0, ""); - g_hSDKWeaponBottle = PrepareItemHandle("tf_weapon_bottle", 1, 0, 0, ""); - - // Heavy - g_hSDKWeaponMinigun = PrepareItemHandle("tf_weapon_minigun", 15, 0, 0, ""); - g_hSDKWeaponShotgunHeavy = PrepareItemHandle("tf_weapon_shotgun", 11, 0, 0, ""); - g_hSDKWeaponFists = PrepareItemHandle("tf_weapon_fists", 5, 0, 0, ""); - - // Medic - g_hSDKWeaponSyringeGun = PrepareItemHandle("tf_weapon_syringegun_medic", 17, 0, 0, ""); - g_hSDKWeaponMedigun = PrepareItemHandle("tf_weapon_medigun", 29, 0, 0, ""); - g_hSDKWeaponBonesaw = PrepareItemHandle("tf_weapon_bonesaw", 8, 0, 0, ""); - - // Pyro - g_hSDKWeaponFlamethrower = PrepareItemHandle("tf_weapon_flamethrower", 21, 0, 0, "254 ; 4.0"); - g_hSDKWeaponShotgunPyro = PrepareItemHandle("tf_weapon_shotgun", 12, 0, 0, ""); - g_hSDKWeaponFireaxe = PrepareItemHandle("tf_weapon_fireaxe", 2, 0, 0, ""); - - // Spy - g_hSDKWeaponRevolver = PrepareItemHandle("tf_weapon_revolver", 24, 0, 0, ""); - g_hSDKWeaponKnife = PrepareItemHandle("tf_weapon_knife", 4, 0, 0, ""); - g_hSDKWeaponInvis = PrepareItemHandle("tf_weapon_invis", 297, 0, 0, ""); - - // Engineer - g_hSDKWeaponShotgunPrimary = PrepareItemHandle("tf_weapon_shotgun", 9, 0, 0, ""); - g_hSDKWeaponPistol = PrepareItemHandle("tf_weapon_pistol", 22, 0, 0, ""); - g_hSDKWeaponWrench = PrepareItemHandle("tf_weapon_wrench", 7, 0, 0, ""); -} - -public OnMapStart() -{ - PvP_OnMapStart(); -} - -public OnConfigsExecuted() -{ - if (!GetConVarBool(g_cvEnabled)) - { - StopPlugin(); - } - else - { - if (GetConVarBool(g_cvSlenderMapsOnly)) - { - decl String:sMap[256]; - GetCurrentMap(sMap, sizeof(sMap)); - - if (!StrContains(sMap, "slender_", false) || !StrContains(sMap, "sf2_", false)) - { - StartPlugin(); - } - else - { - LogMessage("%s is not a Slender Fortress map. Plugin disabled!", sMap); - StopPlugin(); - } - } - else - { - StartPlugin(); - } - } -} - -static StartPlugin() -{ - if (g_bEnabled) return; - - g_bEnabled = true; - - InitializeLogging(); - -#if defined DEBUG - InitializeDebugLogging(); -#endif - - // Handle ConVars. - new Handle:hCvar = FindConVar("mp_friendlyfire"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mp_flashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mat_supportflashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mp_autoteambalance"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - g_flGravity = GetConVarFloat(g_cvGravity); - - g_b20Dollars = GetConVarBool(g_cv20Dollars); - - g_bPlayerShakeEnabled = GetConVarBool(g_cvPlayerShakeEnabled); - g_bPlayerViewbobEnabled = GetConVarBool(g_cvPlayerViewbobEnabled); - g_bPlayerViewbobHurtEnabled = GetConVarBool(g_cvPlayerViewbobHurtEnabled); - g_bPlayerViewbobSprintEnabled = GetConVarBool(g_cvPlayerViewbobSprintEnabled); - - decl String:sBuffer[64]; - Format(sBuffer, sizeof(sBuffer), "RYTP Horror", PLUGIN_VERSION_DISPLAY); - Steam_SetGameDescription(sBuffer); - - PrecacheStuff(); - - // Reset special round. - g_bSpecialRound = false; - g_bSpecialRoundNew = false; - g_bSpecialRoundContinuous = false; - g_iSpecialRoundCount = 1; - g_iSpecialRoundType = 0; - - SpecialRoundReset(); - - // Reset boss rounds. - g_bNewBossRound = false; - g_bNewBossRoundNew = false; - g_bNewBossRoundContinuous = false; - g_iNewBossRoundCount = 1; - strcopy(g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile), ""); - - // Reset global round vars. - g_iRoundCount = 0; - g_iRoundEndCount = 0; - g_iRoundActiveCount = 0; - g_iRoundState = SF2RoundState_Invalid; - g_hRoundMessagesTimer = CreateTimer(200.0, Timer_RoundMessages, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_iRoundMessagesNum = 0; - - g_iRoundWarmupRoundCount = 0; - - g_hClientAverageUpdateTimer = CreateTimer(0.2, Timer_ClientAverageUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_hBossCountUpdateTimer = CreateTimer(2.0, Timer_BossCountUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - SetRoundState(SF2RoundState_Waiting); - - ReloadBossProfiles(); - ReloadRestrictedWeapons(); - ReloadSpecialRounds(); - - NPCOnConfigsExecuted(); - - InitializeBossPackVotes(); - SetupTimeLimitTimerForBossPackVote(); - - // Late load compensation. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - OnClientPutInServer(i); - } -} - -static PrecacheStuff() -{ - // Initialize particles. - g_iParticleCriticalHit = PrecacheParticleSystem(CRIT_PARTICLENAME); - - PrecacheSound2(CRIT_SOUND); - - // simple_bot; - PrecacheModel("models/humans/group01/female_01.mdl", true); - - PrecacheModel(PAGE_MODEL, true); - PrecacheModel(GHOST_MODEL, true); - - PrecacheSound2(FLASHLIGHT_CLICKSOUND); - PrecacheSound2(FLASHLIGHT_BREAKSOUND); - PrecacheSound2(FLASHLIGHT_NOSOUND); - PrecacheSound2(PAGE_GRABSOUND); - - PrecacheSound2(MUSIC_GOTPAGES1_SOUND); - PrecacheSound2(MUSIC_GOTPAGES2_SOUND); - PrecacheSound2(MUSIC_GOTPAGES3_SOUND); - PrecacheSound2(MUSIC_GOTPAGES4_SOUND); - - PrecacheSound2(SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); - - for (new i = 0; i < sizeof(g_strPlayerBreathSounds); i++) - { - PrecacheSound2(g_strPlayerBreathSounds[i]); - } - - for (new i = 0; i < sizeof(g_strGhostHelpPhrases); i++) - { - PrecacheSound2(g_strGhostHelpPhrases[i]); - } - - for (new i = 0; i < sizeof(g_sOverlayMat); i++) - { - new String:path[PLATFORM_MAX_PATH]; - //strcopy(path, sizeof(path), g_sOverlayMat[i][10]); - Format(path, sizeof(path), "%s.vmt", g_sOverlayMat[i]); - PrecacheModel(path); - Format(path, sizeof(path), "%s.vtf", g_sOverlayMat[i]); - PrecacheModel(path); - //PrintToServer("PrecacheMaterial2('%s');", path); - } - PrecacheMaterial2(STATIC_OVERLAY); - PrecacheSound2(STATIC_SOUND); - - // Special round. - PrecacheSound2(SR_MUSIC); - PrecacheSound2(SR_SOUND_SELECT); - PrecacheSound2(SF2_INTRO_DEFAULT_MUSIC); - - PrecacheMaterial2(GRAIN_OVERLAY); - - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.mdl"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx80.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx90.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.phy"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.sw.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.vvd"); - - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vmt"); - - // pvp - PvP_Precache(); -} - -static StopPlugin() -{ - if (!g_bEnabled) return; - - g_bEnabled = false; - - // Reset CVars. - new Handle:hCvar = FindConVar("mp_friendlyfire"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - hCvar = FindConVar("mp_flashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - hCvar = FindConVar("mat_supportflashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - // Cleanup bosses. - NPCRemoveAll(); - - // Cleanup clients. - for (new i = 1; i <= MaxClients; i++) - { - ClientResetFlashlight(i); - ClientDeactivateUltravision(i); - ClientDisableConstantGlow(i); - ClientRemoveInteractiveGlow(i); - } - - ForceTeamWin(_:TFTeam_Blue); - - BossProfilesOnMapEnd(); -} - -public OnMapEnd() -{ - StopPlugin(); -} - -public OnMapTimeLeftChanged() -{ - if (g_bEnabled) - { - SetupTimeLimitTimerForBossPackVote(); - } -} - -public TF2_OnConditionAdded(client, TFCond:cond) -{ - if (cond == TFCond_Taunting) - { - if (IsClientInGhostMode(client)) - { - // Stop ghosties from taunting. - TF2_RemoveCondition(client, TFCond_Taunting); - } - } -} - -public OnGameFrame() -{ - if (!g_bEnabled) return; - - // Process through boss movement. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new iBoss = NPCGetEntIndex(i); - if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; - - if (NPCGetFlags(i) & SFF_MARKEDASFAKE) continue; - - new iType = NPCGetType(i); - - switch (iType) - { - case SF2BossType_Static: - { - decl Float:myPos[3], Float:hisPos[3]; - SlenderGetAbsOrigin(i, myPos); - AddVectors(myPos, g_flSlenderEyePosOffset[i], myPos); - - new iBestPlayer = -1; - new Float:flBestDistance = 16384.0; - new Float:flTempDistance; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !IsPlayerAlive(iClient) || IsClientInGhostMode(iClient) || IsClientInDeathCam(iClient)) continue; - if (!IsPointVisibleToPlayer(iClient, myPos, false, false)) continue; - - GetClientAbsOrigin(iClient, hisPos); - - flTempDistance = GetVectorDistance(myPos, hisPos); - if (flTempDistance < flBestDistance) - { - iBestPlayer = iClient; - flBestDistance = flTempDistance; - } - } - - if (iBestPlayer > 0) - { - SlenderGetAbsOrigin(i, myPos); - GetClientAbsOrigin(iBestPlayer, hisPos); - - if (!SlenderOnlyLooksIfNotSeen(i) || !IsPointVisibleToAPlayer(myPos, false, SlenderUsesBlink(i))) - { - new Float:flTurnRate = NPCGetTurnRate(i); - - if (flTurnRate > 0.0) - { - decl Float:flMyEyeAng[3], Float:ang[3]; - GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); - AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[i], flMyEyeAng); - SubtractVectors(hisPos, myPos, ang); - GetVectorAngles(ang, ang); - ang[0] = 0.0; - ang[1] += (AngleDiff(ang[1], flMyEyeAng[1]) >= 0.0 ? 1.0 : -1.0) * flTurnRate * GetTickInterval(); - ang[2] = 0.0; - - // Take care of angle offsets. - AddVectors(ang, g_flSlenderEyePosOffset[i], ang); - for (new i2 = 0; i2 < 3; i2++) ang[i2] = AngleNormalize(ang[i2]); - - TeleportEntity(iBoss, NULL_VECTOR, ang, NULL_VECTOR); - } - } - } - } - case SF2BossType_Chaser: - { - SlenderChaseBossProcessMovement(i); - } - } - } - - PvP_OnGameFrame(); -} - -// ========================================================== -// COMMANDS AND COMMAND HOOK FUNCTIONS -// ========================================================== - -public Action:Command_Help(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuHelp, client, 30); - return Plugin_Handled; -} - -public Action:Command_Settings(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuSettings, client, 30); - return Plugin_Handled; -} - -public Action:Command_Credits(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuCredits, client, MENU_TIME_FOREVER); - return Plugin_Handled; -} - -public Action:Command_ToggleFlashlight(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return Plugin_Handled; - - if (!IsRoundInWarmup() && !IsRoundInIntro() && !IsRoundEnding() && !DidClientEscape(client)) - { - if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) - { - ClientHandleFlashlight(client); - } - } - - return Plugin_Handled; -} - -public Action:Command_SprintOn(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) - { - ClientHandleSprint(client, true); - } - - return Plugin_Handled; -} - -public Action:Command_SprintOff(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) - { - ClientHandleSprint(client, false); - } - - return Plugin_Handled; -} - -public Action:Command_MainMenu(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuMain, client, 30); - return Plugin_Handled; -} - -public Action:Command_Next(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayQueuePointsMenu(client); - return Plugin_Handled; -} - -public Action:Command_Group(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayGroupMainMenuToClient(client); - return Plugin_Handled; -} - -public Action:Command_GroupName(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_slgroupname <name>"); - return Plugin_Handled; - } - - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return Plugin_Handled; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return Plugin_Handled; - } - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetCmdArg(1, sGroupName, sizeof(sGroupName)); - if (!sGroupName[0]) - { - CPrintToChat(client, "%T", "SF2 Invalid Group Name", client); - return Plugin_Handled; - } - - decl String:sOldGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sOldGroupName, sizeof(sOldGroupName)); - SetPlayerGroupName(iGroupIndex, sGroupName); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) != iGroupIndex) continue; - CPrintToChat(i, "%T", "SF2 Group Name Set", i, sOldGroupName, sGroupName); - } - - return Plugin_Handled; -} - -public Action:Command_GhostMode(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuGhostMode, client, 15); - return Plugin_Handled; -} - -public Action:Hook_CommandSay(client, const String:command[], argc) -{ - if (!g_bEnabled || GetConVarBool(g_cvAllChat)) return Plugin_Continue; - - if (!IsRoundEnding()) - { - if (g_bPlayerEliminated[client]) - { - decl String:sMessage[256]; - GetCmdArgString(sMessage, sizeof(sMessage)); - FakeClientCommand(client, "say_team %s", sMessage); - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Hook_CommandSuicideAttempt(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) return Plugin_Handled; - - if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; - - if (GetConVarBool(g_cvBlockSuicideDuringRound)) - { - if (!g_bRoundGrace && !g_bPlayerEliminated[client] && !DidClientEscape(client)) - { - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Hook_CommandBlockInGhostMode(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) return Plugin_Handled; - if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Hook_CommandVoiceMenu(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) - { - ClientGhostModeNextTarget(client); - return Plugin_Handled; - } - - if (g_bPlayerProxy[client]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) - { - return Plugin_Handled; - } - } - } - - return Plugin_Continue; -} - -public Action:Command_ClientPerformScare(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_scare <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32], String:arg2[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - GetCmdArg(2, arg2, sizeof(arg2)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - COMMAND_FILTER_ALIVE, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - ClientPerformScare(target, StringToInt(arg2)); - } - - return Plugin_Handled; -} - -public Action:Command_SpawnSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args == 0) - { - ReplyToCommand(client, "Usage: sm_sf2_spawn_boss <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl Float:eyePos[3], Float:eyeAng[3], Float:endPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(endPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, endPos); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Spawned Boss", client); - LogAction(client, -1, "%N spawned boss %d! (%s)", client, iBossIndex, sProfile); - - return Plugin_Handled; -} - -public Action:Command_RemoveSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args == 0) - { - ReplyToCommand(client, "Usage: sm_sf2_remove_boss <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - NPCRemove(iBossIndex); - - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Removed Boss", client); - LogAction(client, -1, "%N removed boss %d! (%s)", client, iBossIndex, sProfile); - - return Plugin_Handled; -} - -public Action:Command_GetBossIndexes(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - decl String:sMessage[512]; - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - ClientCommand(client, "echo Active Boss Indexes:"); - ClientCommand(client, "echo ----------------------------"); - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - Format(sMessage, sizeof(sMessage), "%d - %s", i, sProfile); - if (NPCGetFlags(i) & SFF_FAKE) - { - StrCat(sMessage, sizeof(sMessage), " (fake)"); - } - - if (g_iSlenderCopyMaster[i] != -1) - { - decl String:sCat[64]; - Format(sCat, sizeof(sCat), " (copy of %d)", g_iSlenderCopyMaster[i]); - StrCat(sMessage, sizeof(sMessage), sCat); - } - - ClientCommand(client, "echo %s", sMessage); - } - - ClientCommand(client, "echo ----------------------------"); - - ReplyToCommand(client, "Printed active boss indexes to your console!"); - - return Plugin_Handled; -} - -public Action:Command_SlenderAttackWaiters(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_boss_attack_waiters <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossFlags = NPCGetFlags(iBossIndex); - - new bool:bState = bool:StringToInt(arg2); - new bool:bOldState = bool:(iBossFlags & SFF_ATTACKWAITERS); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (bState) - { - if (!bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags | SFF_ATTACKWAITERS); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Attack Waiters", client); - LogAction(client, -1, "%N forced boss %d to attack waiters! (%s)", client, iBossIndex, sProfile); - } - } - else - { - if (bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags & ~SFF_ATTACKWAITERS); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Do Not Attack Waiters", client); - LogAction(client, -1, "%N forced boss %d to not attack waiters! (%s)", client, iBossIndex, sProfile); - } - } - - return Plugin_Handled; -} - -public Action:Command_SlenderNoTeleport(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_boss_no_teleport <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossFlags = NPCGetFlags(iBossIndex); - - new bool:bState = bool:StringToInt(arg2); - new bool:bOldState = bool:(iBossFlags & SFF_NOTELEPORT); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (bState) - { - if (!bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags | SFF_NOTELEPORT); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Not Teleport", client); - LogAction(client, -1, "%N disabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); - } - } - else - { - if (bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags & ~SFF_NOTELEPORT); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Teleport", client); - LogAction(client, -1, "%N enabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); - } - } - - return Plugin_Handled; -} - -public Action:Command_ForceProxy(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_force_proxy <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - if (IsRoundEnding() || IsRoundInWarmup()) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - 0, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossIndex = StringToInt(arg2); - if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) - { - ReplyToCommand(client, "Boss index is out of range!"); - return Plugin_Handled; - } - else if (NPCGetUniqueID(iBossIndex) == -1) - { - ReplyToCommand(client, "Boss index is invalid! Boss index not active!"); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new iTarget = target_list[i]; - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(iTarget, sName, sizeof(sName)); - - if (!g_bPlayerEliminated[iTarget]) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Unable To Perform Action On Player In Round", client, sName); - continue; - } - - if (g_bPlayerProxy[iTarget]) continue; - - decl Float:flNewPos[3]; - - if (!SlenderCalculateNewPlace(iBossIndex, flNewPos, true, true, client)) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Player No Place For Proxy", client, sName); - continue; - } - - ClientEnableProxy(iTarget, iBossIndex); - TeleportEntity(iTarget, flNewPos, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - - LogAction(client, iTarget, "%N forced %N to be a Proxy!", client, iTarget); - } - - return Plugin_Handled; -} - -public Action:Command_ForceEscape(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_force_escape <name|#userid>"); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - COMMAND_FILTER_ALIVE, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - if (!g_bPlayerEliminated[i] && !DidClientEscape(i)) - { - ClientEscape(target); - TeleportClientToEscapePoint(target); - - LogAction(client, target, "%N forced %N to escape!", client, target); - } - } - - return Plugin_Handled; -} - -public Action:Command_AddSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_add_boss <name>"); - return Plugin_Handled; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetCmdArg(1, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - if (!KvJumpToKey(g_hConfig, sProfile)) - { - ReplyToCommand(client, "That boss does not exist!"); - return Plugin_Handled; - } - - new iBossIndex = AddProfile(sProfile); - if (iBossIndex != -1) - { - decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(flPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, flPos); - - LogAction(client, -1, "%N added a boss! (%s)", client, sProfile); - } - - return Plugin_Handled; -} - -public Action:Command_AddSlenderFake(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_add_boss_fake <name>"); - return Plugin_Handled; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetCmdArg(1, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - if (!KvJumpToKey(g_hConfig, sProfile)) - { - ReplyToCommand(client, "That boss does not exist!"); - return Plugin_Handled; - } - - new iBossIndex = AddProfile(sProfile, SFF_FAKE); - if (iBossIndex != -1) - { - decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(flPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, flPos); - - LogAction(client, -1, "%N added a fake boss! (%s)", client, sProfile); - } - - return Plugin_Handled; -} - -public Action:Command_ForceState(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_setplaystate <name|#userid> <0/1>"); - return Plugin_Handled; - } - - if (IsRoundEnding() || IsRoundInWarmup()) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - 0, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iState = StringToInt(arg2); - - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - GetClientName(target, sName, sizeof(sName)); - - if (iState && g_bPlayerEliminated[target]) - { - SetClientPlayState(target, true); - - CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced In Game", sName); - LogAction(client, target, "%N forced %N into the game.", client, target); - } - else if (!iState && !g_bPlayerEliminated[target]) - { - SetClientPlayState(target, false); - - CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced Out Of Game", sName); - LogAction(client, target, "%N took %N out of the game.", client, target); - } - } - - return Plugin_Handled; -} - -public Action:Hook_CommandBuild(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (!IsClientInPvP(client)) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Timer_BossCountUpdate(Handle:timer) -{ - if (timer != g_hBossCountUpdateTimer) return Plugin_Stop; - - if (!g_bEnabled) return Plugin_Stop; - - new iBossCount = NPCGetCount(); - new iBossPreferredCount; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1 || - g_iSlenderCopyMaster[i] != -1 || - (NPCGetFlags(i) & SFF_FAKE)) - { - continue; - } - - iBossPreferredCount++; - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - IsClientInGhostMode(i) || - IsClientInDeathCam(i) || - DidClientEscape(i)) continue; - - // Check if we're near any bosses. - new iClosest = -1; - new Float:flBestDist = SF2_BOSS_PAGE_CALCULATION; - - for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) - { - if (NPCGetUniqueID(iBoss) == -1) continue; - if (NPCGetEntIndex(iBoss) == INVALID_ENT_REFERENCE) continue; - if (NPCGetFlags(iBoss) & SFF_FAKE) continue; - - new Float:flDist = NPCGetDistanceFromEntity(iBoss, i); - if (flDist < flBestDist) - { - iClosest = iBoss; - flBestDist = flDist; - break; - } - } - - if (iClosest != -1) continue; - - iClosest = -1; - flBestDist = SF2_BOSS_PAGE_CALCULATION; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient) || - !IsPlayerAlive(iClient) || - g_bPlayerEliminated[iClient] || - IsClientInGhostMode(iClient) || - IsClientInDeathCam(iClient) || - DidClientEscape(iClient)) - { - continue; - } - - new bool:bwub = false; - for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) - { - if (NPCGetUniqueID(iBoss) == -1) continue; - if (NPCGetFlags(iBoss) & SFF_FAKE) continue; - - if (g_iSlenderTarget[iBoss] == iClient) - { - bwub = true; - break; - } - } - - if (!bwub) continue; - - new Float:flDist = EntityDistanceFromEntity(i, iClient); - if (flDist < flBestDist) - { - iClosest = iClient; - flBestDist = flDist; - } - } - - if (!IsValidClient(iClosest)) - { - // No one's close to this dude? DUDE! WE NEED ANOTHER BOSS! - iBossPreferredCount++; - } - } - - new iDiff = iBossCount - iBossPreferredCount; - if (iDiff) - { - if (iDiff > 0) - { - new iCount = iDiff; - // We need less bosses. Try and see if we can remove some. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_iSlenderCopyMaster[i] == -1) continue; - if (PeopleCanSeeSlender(i, _, false)) continue; - if (NPCGetFlags(i) & SFF_FAKE) continue; - - if (SlenderCanRemove(i)) - { - NPCRemove(i); - iCount--; - } - - if (iCount <= 0) - { - break; - } - } - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - new iCount = RoundToFloor(FloatAbs(float(iDiff))); - // Add new bosses (copy of the first boss). - for (new i = 0; i < MAX_BOSSES && iCount > 0; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - if (g_iSlenderCopyMaster[i] != -1) continue; - if (!(NPCGetFlags(i) & SFF_COPIES)) continue; - - // Get the number of copies I already have and see if I can have more copies. - new iCopyCount; - for (new i2 = 0; i2 < MAX_BOSSES; i2++) - { - if (NPCGetUniqueID(i2) == -1) continue; - if (g_iSlenderCopyMaster[i2] != i) continue; - - iCopyCount++; - } - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - if (iCopyCount >= GetProfileNum(sProfile, "copy_max", 10)) - { - continue; - } - - new iBossIndex = AddProfile(sProfile, _, i); - if (iBossIndex == -1) - { - LogError("Could not add copy for %d: No free slots!", i); - } - - iCount--; - } - } - } - - // Check if we can add some proxies. - if (!g_bRoundGrace) - { - if (NavMesh_Exists()) - { - new Handle:hProxyCandidates = CreateArray(); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) continue; - - if (g_iSlenderCopyMaster[iBossIndex] != -1) continue; // Copies cannot generate proxies. - - if (GetGameTime() < g_flSlenderTimeUntilNextProxy[iBossIndex]) continue; // Proxy spawning hasn't cooled down yet. - - new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); - if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) continue; // No teleport target. - - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumActiveProxies = 0; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) == iBossIndex) - { - iNumActiveProxies++; - } - } - - if (iNumActiveProxies >= iMaxProxies) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d has too many active proxies!", iBossIndex); -#endif - continue; - } - - new Float:flSpawnChanceMin = GetProfileFloat(sProfile, "proxies_spawn_chance_min"); - new Float:flSpawnChanceMax = GetProfileFloat(sProfile, "proxies_spawn_chance_max"); - new Float:flSpawnChanceThreshold = GetProfileFloat(sProfile, "proxies_spawn_chance_threshold") * NPCGetAnger(iBossIndex); - - new Float:flChance = GetRandomFloat(flSpawnChanceMin, flSpawnChanceMax); - if (flChance > flSpawnChanceThreshold) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's chances weren't in his favor!", iBossIndex); -#endif - continue; - } - - new iAvailableProxies = iMaxProxies - iNumActiveProxies; - - new iSpawnNumMin = GetProfileNum(sProfile, "proxies_spawn_num_min"); - new iSpawnNumMax = GetProfileNum(sProfile, "proxies_spawn_num_max"); - - new iSpawnNum = 0; - - // Get a list of people we can transform into a good Proxy. - ClearArray(hProxyCandidates); - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (g_bPlayerProxy[iClient]) continue; - - if (!g_iPlayerPreferences[iClient][PlayerPreference_EnableProxySelection]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your preferences.", iBossIndex); -#endif - continue; - } - - if (!g_bPlayerProxyAvailable[iClient]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your cooldown.", iBossIndex); -#endif - continue; - } - - if (g_bPlayerProxyAvailableInForce[iClient]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're already being forced into a Proxy.", iBossIndex); -#endif - continue; - } - - if (!IsClientParticipating(iClient)) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're not participating.", iBossIndex); -#endif - continue; - } - - PushArrayCell(hProxyCandidates, iClient); - iSpawnNum++; - } - - if (iSpawnNum >= iSpawnNumMax) - { - iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNumMax); - } - else if (iSpawnNum >= iSpawnNumMin) - { - iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNum); - } - - if (iSpawnNum <= 0) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d had a set spawn number of 0!", iBossIndex); -#endif - continue; - } - - decl Float:flTargetPos[3]; - GetClientAbsOrigin(iTeleportTarget, flTargetPos); - - new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); - if (iTargetAreaIndex == -1) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's teleport target is not on the navmesh!", iBossIndex); -#endif - continue; // target is not on the nav mesh. - } - - // Search outwards until travel distance is at maximum range. - new Handle:hAreaArray = CreateArray(2); - new Handle:hAreas = CreateStack(); - NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderProxyTeleportMaxRange[iBossIndex]); - - new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderProxyTeleportMinRange[iBossIndex], g_flSlenderProxyTeleportMaxRange[iBossIndex]); - - { - new iAreaIndex = -1; - new iPoppedAreas = 0; - - while (!IsStackEmpty(hAreas)) - { - PopStackCell(hAreas, iAreaIndex); - new iCostSoFar = NavMeshArea_GetCostSoFar(iAreaIndex); - - if (float(iCostSoFar) >= flTeleportMinRange) - { - new iIndex = PushArrayCell(hAreaArray, iAreaIndex); - SetArrayCell(hAreaArray, iIndex, float(iCostSoFar), 1); - iPoppedAreas++; - } - } - - CloseHandle(hAreas); - - if (iPoppedAreas == 0) - { - // no areas to use! - CloseHandle(hAreaArray); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any sufficient surrounding areas!", iBossIndex); -#endif - - continue; - } -#if defined DEBUG - else - { - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d found %d surrounding areas", iBossIndex, iPoppedAreas); - } -#endif - } - - new Handle:hAreaArrayClose = CreateArray(); - new Handle:hAreaArrayAverage = CreateArray(); - new Handle:hAreaArrayFar = CreateArray(); - - for (new iRangeSection = 1; iRangeSection <= 3; iRangeSection++) - { - new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection - 1) / 3.0); - new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection) / 3.0); - - for (new i = 0, iSize = GetArraySize(hAreaArray); i < iSize; i++) - { - new iAreaIndex = GetArrayCell(hAreaArray, i); - - decl Float:flAreaCenter[3]; - NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); - - decl Float:flTestPos[3]; - decl Float:flEyeOffset[3]; - flEyeOffset[0] = 0.0; - flEyeOffset[1] = 0.0; - flEyeOffset[2] = HalfHumanHeight * 2.0; - - // Check visibility first. - if (IsPointVisibleToAPlayer(flAreaCenter, false, false)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (1)", iBossIndex, iAreaIndex); -#endif - continue; - } - - AddVectors(flAreaCenter, flEyeOffset, flTestPos); - - if (IsPointVisibleToAPlayer(flTestPos, false, false)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (2)", iBossIndex, iAreaIndex); -#endif - - continue; - } - - new iBoss = NPCGetEntIndex(iBossIndex); - - // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. - { - decl Float:flTraceStartPos[3]; - flTraceStartPos[0] = flAreaCenter[0]; - flTraceStartPos[1] = flAreaCenter[1]; - flTraceStartPos[2] = flAreaCenter[2] + (HalfHumanHeight * 2.0); - - decl Float:flTraceMins[3]; - flTraceMins[0] = -20.0; - flTraceMins[1] = -20.0; - flTraceMins[2] = 0.0; - - decl Float:flTraceMaxs[3]; - flTraceMaxs[0] = 20.0; - flTraceMaxs[1] = 20.0; - flTraceMaxs[2] = 0.0; - - new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flAreaCenter, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - decl Float:flTraceHitPos[3]; - TR_GetEndPosition(flTraceHitPos, hTrace); - flTraceHitPos[2] += 1.0; - CloseHandle(hTrace); - - static Float:flTraceSpaceMin[3] = { -20.0, -20.0, 0.0 }; - static Float:flTraceSpaceMax[3] = { 20.0, 20.0, 72.0 }; - - flTraceSpaceMax[2] = HalfHumanHeight * 2.0; - - if (IsSpaceOccupiedPlayer(flTraceHitPos, - flTraceSpaceMin, - flTraceSpaceMax, - iBoss == INVALID_ENT_REFERENCE ? -1 : iBoss)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected too small area index %d! (2)", iBossIndex, iAreaIndex); -#endif - - continue; - } - } - - new bool:bTooNear = false; - - // Check minimum range. - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || - !IsPlayerAlive(iClient) || - g_bPlayerEliminated[iClient] || - DidClientEscape(iClient) || - g_bPlayerProxy[iClient] || - IsClientInGhostMode(iClient)) - { - continue; - } - - decl Float:flTempPos[3]; - GetClientAbsOrigin(iClient, flTempPos); - - if (GetVectorDistance(flAreaCenter, flTempPos) <= flTeleportMinRange) - { - bTooNear = true; - break; - } - } - - if (bTooNear) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected near area index %d!", iBossIndex, iAreaIndex); -#endif - - continue; // This area is too close to a player. - } - - // Check travel distance. - new Float:flDist = Float:GetArrayCell(hAreaArray, i, 1); - if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) - { - switch (iRangeSection) - { - case 1: PushArrayCell(hAreaArrayClose, iAreaIndex); - case 2: PushArrayCell(hAreaArrayAverage, iAreaIndex); - case 3: PushArrayCell(hAreaArrayFar, iAreaIndex); - } - } - } - } - - CloseHandle(hAreaArray); - - // Set the cooldown time! - new Float:flSpawnCooldownMin = GetProfileFloat(sProfile, "proxies_spawn_cooldown_min"); - new Float:flSpawnCooldownMax = GetProfileFloat(sProfile, "proxies_spawn_cooldown_max"); - - g_flSlenderTimeUntilNextProxy[iBossIndex] = GetGameTime() + GetRandomFloat(flSpawnCooldownMin, flSpawnCooldownMax); - - // Randomize the array. - SortADTArray(hProxyCandidates, Sort_Random, Sort_Integer); - - decl Float:flDestinationPos[3]; - - for (new iNum = 0; iNum < iSpawnNum && iNum < iAvailableProxies; iNum++) - { - new iClient = GetArrayCell(hProxyCandidates, iNum); - new iBestAreaIndex = -1; - - if (GetArraySize(hAreaArrayClose) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayClose, GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1)); - } - else if (GetArraySize(hAreaArrayAverage) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayAverage, GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1)); - } - else if (GetArraySize(hAreaArrayFar) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayFar, GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1)); - } - - if (iBestAreaIndex == -1) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any areas to place proxies (spawned %d)!", iBossIndex, iNum); -#endif - break; - } - - NavMeshArea_GetCenter(iBestAreaIndex, flDestinationPos); - - if (!GetConVarBool(g_cvPlayerProxyAsk)) - { - ClientStartProxyForce(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); - } - else - { - DisplayProxyAskMenu(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); - } - } - - CloseHandle(hAreaArrayClose); - CloseHandle(hAreaArrayAverage); - CloseHandle(hAreaArrayFar); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d finished proxy process!", iBossIndex); -#endif - } - - CloseHandle(hProxyCandidates); - } - } - - return Plugin_Continue; -} - -ReloadRestrictedWeapons() -{ - if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) - { - CloseHandle(g_hRestrictedWeaponsConfig); - g_hRestrictedWeaponsConfig = INVALID_HANDLE; - } - - decl String:buffer[PLATFORM_MAX_PATH]; - BuildPath(Path_SM, buffer, sizeof(buffer), FILE_RESTRICTEDWEAPONS); - new Handle:kv = CreateKeyValues("root"); - if (!FileToKeyValues(kv, buffer)) - { - CloseHandle(kv); - LogError("Failed to load restricted weapons list! File not found!"); - } - else - { - g_hRestrictedWeaponsConfig = kv; - LogSF2Message("Reloaded restricted weapons configuration file successfully"); - } -} - -public Action:Timer_RoundMessages(Handle:timer) -{ - if (!g_bEnabled) return Plugin_Stop; - - if (timer != g_hRoundMessagesTimer) return Plugin_Stop; - - switch (g_iRoundMessagesNum) - { - case 0: CPrintToChatAll("{lightgreen}Slender Fortress v %s{olive} by {lightgreen}Kit o' Rifty{olive}. {lightgreen}RYTP Horror {olive}edit by {lightgreen}lexuzieel", PLUGIN_VERSION_DISPLAY); - case 1: CPrintToChatAll("%t", "SF2 Ad Message 1"); - case 2: CPrintToChatAll("%t", "SF2 Ad Message 2"); - case 3: CPrintToChatAll("%t", "SF2 Ad Message 3"); - } - - g_iRoundMessagesNum++; - if (g_iRoundMessagesNum > 3) g_iRoundMessagesNum = 0; - - return Plugin_Continue; -} - -public Action:Timer_WelcomeMessage(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - CPrintToChat(client, "%T", "SF2 Welcome Message", client); -} - -GetMaxPlayersForRound() -{ - new iOverride = GetConVarInt(g_cvMaxPlayersOverride); - if (iOverride != -1) return iOverride; - return GetConVarInt(g_cvMaxPlayers); -} - -public OnConVarChanged(Handle:cvar, const String:oldValue[], const String:newValue[]) -{ - if (cvar == g_cvDifficulty) - { - switch (StringToInt(newValue)) - { - case Difficulty_Easy: g_flRoundDifficultyModifier = DIFFICULTY_EASY; - case Difficulty_Hard: g_flRoundDifficultyModifier = DIFFICULTY_HARD; - case Difficulty_Insane: g_flRoundDifficultyModifier = DIFFICULTY_INSANE; - default: g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; - } - } - else if (cvar == g_cvMaxPlayers || cvar == g_cvMaxPlayersOverride) - { - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - CheckPlayerGroup(i); - } - } - else if (cvar == g_cvPlayerShakeEnabled) - { - g_bPlayerShakeEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobEnabled) - { - g_bPlayerViewbobEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobHurtEnabled) - { - g_bPlayerViewbobHurtEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobSprintEnabled) - { - g_bPlayerViewbobSprintEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvGravity) - { - g_flGravity = StringToFloat(newValue); - } - else if (cvar == g_cv20Dollars) - { - g_b20Dollars = bool:StringToInt(newValue); - } - else if (cvar == g_cvAllChat) - { - if (g_bEnabled) - { - for (new i = 1; i <= MaxClients; i++) - { - ClientUpdateListeningFlags(i); - } - } - } -} - -// ========================================================== -// IN-GAME AND ENTITY HOOK FUNCTIONS -// ========================================================== - - -public OnEntityCreated(ent, const String:classname[]) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(ent) || ent <= 0) return; - - if (StrEqual(classname, "spotlight_end", false)) - { - SDKHook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); - } - else if (StrEqual(classname, "beam", false)) - { - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightBeamSetTransmit); - } - - PvP_OnEntityCreated(ent, classname); -} - -public OnEntityDestroyed(ent) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(ent) || ent <= 0) return; - - decl String:sClassname[64]; - GetEntityClassname(ent, sClassname, sizeof(sClassname)); - - if (StrEqual(sClassname, "light_dynamic", false)) - { - AcceptEntityInput(ent, "TurnOff"); - - new iEnd = INVALID_ENT_REFERENCE; - while ((iEnd = FindEntityByClassname(iEnd, "spotlight_end")) != -1) - { - if (GetEntPropEnt(iEnd, Prop_Data, "m_hOwnerEntity") == ent) - { - AcceptEntityInput(iEnd, "Kill"); - break; - } - } - } - - PvP_OnEntityDestroyed(ent, sClassname); -} - -public Action:Hook_BlockUserMessage(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) -{ - if (!g_bEnabled) return Plugin_Continue; - return Plugin_Handled; -} - -public Action:Hook_NormalSound(clients[64], &numClients, String:sample[PLATFORM_MAX_PATH], &entity, &channel, &Float:volume, &level, &pitch, &flags) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsValidClient(entity)) - { - if (IsClientInGhostMode(entity)) - { - switch (channel) - { - case SNDCHAN_VOICE, SNDCHAN_WEAPON, SNDCHAN_ITEM, SNDCHAN_BODY: return Plugin_Handled; - } - } - else if (g_bPlayerProxy[entity]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[entity]); - if (iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - switch (channel) - { - case SNDCHAN_VOICE: - { - if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) - { - return Plugin_Handled; - } - } - } - } - } - else if (!g_bPlayerEliminated[entity]) - { - switch (channel) - { - case SNDCHAN_VOICE: - { - if (IsRoundInIntro()) return Plugin_Handled; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Voice)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDVOICE; - } - } - } - case SNDCHAN_BODY: - { - if (!StrContains(sample, "player/footsteps", false) || StrContains(sample, "step", false) != -1) - { - if (GetConVarBool(g_cvPlayerViewbobSprintEnabled) && IsClientReallySprinting(entity)) - { - // Viewpunch. - new Float:flPunchVelStep[3]; - - decl Float:flVelocity[3]; - GetEntPropVector(entity, Prop_Data, "m_vecAbsVelocity", flVelocity); - new Float:flSpeed = GetVectorLength(flVelocity); - - flPunchVelStep[0] = flSpeed / 300.0; - flPunchVelStep[1] = 0.0; - flPunchVelStep[2] = 0.0; - - ClientViewPunch(entity, flPunchVelStep); - } - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Footstep)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEP; - - if (IsClientSprinting(entity) && !(GetEntProp(entity, Prop_Send, "m_bDucking") || GetEntProp(entity, Prop_Send, "m_bDucked"))) - { - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEPLOUD; - } - } - } - } - } - case SNDCHAN_ITEM, SNDCHAN_WEAPON: - { - if (StrContains(sample, "impact", false) != -1 || StrContains(sample, "hit", false) != -1) - { - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Weapon)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDWEAPON; - } - } - } - } - } - } - } - /* - new bool:bModified = false; - - for (new i = 0; i < numClients; i++) - { - new iClient = clients[i]; - if (IsValidClient(iClient) && IsPlayerAlive(iClient) && !IsClientInGhostMode(iClient)) - { - new bool:bCanHearSound = true; - - if (IsValidClient(entity) && entity != iClient) - { - if (!g_bPlayerEliminated[iClient]) - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (!g_bPlayerEliminated[entity] && !DidClientEscape(entity)) - { - bCanHearSound = false; - } - } - } - } - - if (!bCanHearSound) - { - bModified = true; - clients[i] = -1; - } - } - } - - if (bModified) return Plugin_Changed; - */ - return Plugin_Continue; -} - -public MRESReturn:Hook_EntityShouldTransmit(thisPointer, Handle:hReturn, Handle:hParams) -{ - if (!g_bEnabled) return MRES_Ignored; - - if (IsValidClient(thisPointer)) - { - if (DoesClientHaveConstantGlow(thisPointer)) - { - DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. - return MRES_Supercede; - } - } - else - { - new iBossIndex = NPCGetFromEntIndex(thisPointer); - if (iBossIndex != -1) - { - DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. - return MRES_Supercede; - } - } - - return MRES_Ignored; -} - -public Hook_TriggerOnStartTouch(const String:output[], caller, activator, Float:delay) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(caller)) return; - - decl String:sName[64]; - GetEntPropString(caller, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrContains(sName, "sf2_escape_trigger", false) == 0) - { - if (IsRoundInEscapeObjective()) - { - if (IsValidClient(activator) && IsPlayerAlive(activator) && !IsClientInDeathCam(activator) && !g_bPlayerEliminated[activator] && !DidClientEscape(activator)) - { - ClientEscape(activator); - TeleportClientToEscapePoint(activator); - } - } - } - - PvP_OnTriggerStartTouch(caller, activator); -} - -public Hook_TriggerOnEndTouch(const String:sOutput[], caller, activator, Float:flDelay) -{ - if (!g_bEnabled) return; - - PvP_OnTriggerEndTouch(caller, activator); -} - -public Action:Hook_PageOnTakeDamage(page, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsValidClient(attacker)) - { - if (!g_bPlayerEliminated[attacker]) - { - if (damagetype & 0x80) // 0x80 == melee damage - { - SetPageCount(g_iPageCount + 1); - g_iPlayerPageCount[attacker] += 1; - EmitSoundToAll(PAGE_GRABSOUND, attacker, SNDCHAN_ITEM, SNDLEVEL_SCREAMING); - - // Gives points. Credit to the makers of VSH/FF2. - new Handle:hEvent = CreateEvent("player_escort_score", true); - SetEventInt(hEvent, "player", attacker); - SetEventInt(hEvent, "points", 1); - FireEvent(hEvent); - - AcceptEntityInput(page, "FireUser1"); - AcceptEntityInput(page, "Kill"); - } - } - } - - return Plugin_Continue; -} - -// ========================================================== -// GENERIC CLIENT HOOKS AND FUNCTIONS -// ========================================================== - - -public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2]) -{ - if (!g_bEnabled) return Plugin_Continue; - - ClientDisableFakeLagCompensation(client); - - // Check impulse (block spraying and built-in flashlight) - switch (impulse) - { - case 100: - { - impulse = 0; - } - case 201: - { - if (IsClientInGhostMode(client)) - { - impulse = 0; - } - } - } - - for (new i = 0; i < MAX_BUTTONS; i++) - { - new button = (1 << i); - - if ((buttons & button)) - { - if (!(g_iPlayerLastButtons[client] & button)) - { - ClientOnButtonPress(client, button); - } - } - else if ((g_iPlayerLastButtons[client] & button)) - { - ClientOnButtonRelease(client, button); - } - } - - g_iPlayerLastButtons[client] = buttons; - - return Plugin_Continue; -} - - -public OnClientCookiesCached(client) -{ - if (!g_bEnabled) return; - - // Load our saved settings. - new String:sCookie[64]; - GetClientCookie(client, g_hCookie, sCookie, sizeof(sCookie)); - - if (!sCookie[0]) - { - g_iPlayerQueuePoints[client] = 0; - - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; - } - else - { - new String:s2[12][32]; - ExplodeString(sCookie, " ; ", s2, 12, 32); - - g_iPlayerQueuePoints[client] = StringToInt(s2[0]); - - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = bool:StringToInt(s2[1]); - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode:StringToInt(s2[2]); - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = bool:StringToInt(s2[4]); - } -} - -public OnClientPutInServer(client) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientPutInServer(%d)", client); -#endif - - ClientSetPlayerGroup(client, -1); - - g_bPlayerEscaped[client] = false; - g_bPlayerEliminated[client] = true; - g_bPlayerChoseTeam[client] = false; - g_bPlayerPlayedSpecialRound[client] = true; - g_bPlayerPlayedNewBossRound[client] = true; - - g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn] = false; - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; - - g_iPlayerPageCount[client] = 0; - g_iPlayerDesiredFOV[client] = 90; - - SDKHook(client, SDKHook_PreThink, Hook_ClientPreThink); - SDKHook(client, SDKHook_SetTransmit, Hook_ClientSetTransmit); - SDKHook(client, SDKHook_OnTakeDamage, Hook_ClientOnTakeDamage); - - DHookEntity(g_hSDKWantsLagCompensationOnEntity, true, client); - DHookEntity(g_hSDKShouldTransmit, true, client); - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - - SetPlayerGroupInvitedPlayer(i, client, false); - SetPlayerGroupInvitedPlayerCount(i, client, 0); - SetPlayerGroupInvitedPlayerTime(i, client, 0.0); - } - - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientUpdateListeningFlags(client); - ClientUpdateMusicSystem(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - ClientResetProxy(client); - ClientResetHints(client); - ClientResetScare(client); - - ClientResetDeathCam(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientSetScareBoostEndTime(client, -1.0); - - ClientStartProxyAvailableTimer(client); - - if (!IsFakeClient(client)) - { - // See if the player is using the projected flashlight. - QueryClientConVar(client, "mat_supportflashlight", OnClientGetProjectedFlashlightSetting); - - // Get desired FOV. - QueryClientConVar(client, "fov_desired", OnClientGetDesiredFOV); - } - - PvP_OnClientPutInServer(client); - -#if defined DEBUG - g_iPlayerDebugFlags[client] = 0; - - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientPutInServer(%d)", client); -#endif -} - -public OnClientGetProjectedFlashlightSetting(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) -{ - if (result != ConVarQuery_Okay) - { - LogError("Warning: Player %N failed to query for ConVar mat_supportflashlight", client); - return; - } - - if (StringToInt(cvarValue)) - { - decl String:sAuth[64]; - GetClientAuthString(client, sAuth, sizeof(sAuth)); - - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = true; - LogSF2Message("Player %N (%s) has mat_supportflashlight enabled, projected flashlight will be used", client, sAuth); - } -} - -public OnClientGetDesiredFOV(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) -{ - if (!IsValidClient(client)) return; - - g_iPlayerDesiredFOV[client] = StringToInt(cvarValue); -} - -public OnClientDisconnect(client) -{ - DestroySpriteOverlay(client); - g_hOverlayUpdateTimer[client] = INVALID_HANDLE; - - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientDisconnect(%d)", client); -#endif - - g_bPlayerEscaped[client] = false; - - // Save and reset settings for the next client. - ClientSaveCookies(client); - ClientSetPlayerGroup(client, -1); - - // Reset variables. - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; - - // Reset any client functions that may be still active. - ClientResetOverlay(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientSetGhostModeState(client, false); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientStopProxyForce(client); - - if (!IsRoundInWarmup()) - { - if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) - { - if (g_bRoundGrace) - { - // Force the next player in queue to take my place, if any. - ForceInNextPlayersInQueue(1, true); - } - else - { - if (!IsRoundEnding()) - { - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - } - - // Reset queue points global variable. - g_iPlayerQueuePoints[client] = 0; - - PvP_OnClientDisconnect(client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientDisconnect(%d)", client); -#endif -} - -public OnClientDisconnect_Post(client) -{ - g_iPlayerLastButtons[client] = 0; -} - -public TF2_OnWaitingForPlayersStart() -{ - g_bRoundWaitingForPlayers = true; -} - -public TF2_OnWaitingForPlayersEnd() -{ - g_bRoundWaitingForPlayers = false; -} - -SF2RoundState:GetRoundState() -{ - return g_iRoundState; -} - -SetRoundState(SF2RoundState:iRoundState) -{ - if (g_iRoundState == iRoundState) return; - - PrintToServer("SetRoundState(%d)", iRoundState); - - new SF2RoundState:iOldRoundState = GetRoundState(); - g_iRoundState = iRoundState; - - // Cleanup from old roundstate if needed. - switch (iOldRoundState) - { - case SF2RoundState_Waiting: - { - } - case SF2RoundState_Intro: - { - g_hRoundIntroTimer = INVALID_HANDLE; - } - case SF2RoundState_Active: - { - g_bRoundGrace = false; - g_hRoundGraceTimer = INVALID_HANDLE; - g_hRoundTimer = INVALID_HANDLE; - } - case SF2RoundState_Escape: - { - g_hRoundTimer = INVALID_HANDLE; - } - case SF2RoundState_Outro: - { - } - } - - switch (g_iRoundState) - { - case SF2RoundState_Waiting: - { - } - case SF2RoundState_Intro: - { - g_hRoundIntroTimer = INVALID_HANDLE; - g_iRoundIntroText = 0; - g_bRoundIntroTextDefault = false; - g_hRoundIntroTextTimer = CreateTimer(0.0, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hRoundIntroTextTimer); - - // Gather data on the intro parameters set by the map. - new Float:flHoldTime = g_flRoundIntroFadeHoldTime; - g_hRoundIntroTimer = CreateTimer(flHoldTime, Timer_ActivateRoundFromIntro, _, TIMER_FLAG_NO_MAPCHANGE); - - // Trigger any intro logic entities, if any. - new ent = -1; - while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) - { - decl String:sName[64]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_intro_relay", false)) - { - AcceptEntityInput(ent, "Trigger"); - break; - } - } - } - case SF2RoundState_Active: - { - // Start the grace period timer. - g_bRoundGrace = true; - g_hRoundGraceTimer = CreateTimer(GetConVarFloat(g_cvGraceTime), Timer_RoundGrace, _, TIMER_FLAG_NO_MAPCHANGE); - - CreateTimer(2.0, Timer_RoundStart, _, TIMER_FLAG_NO_MAPCHANGE); - - // Enable movement on players. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - SetEntityFlags(i, GetEntityFlags(i) & ~FL_FROZEN); - } - - // Fade in. - new Float:flFadeTime = g_flRoundIntroFadeDuration; - new iFadeFlags = SF_FADE_IN | FFADE_PURGE; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - UTIL_ScreenFade(i, FixedUnsigned16(flFadeTime, 1 << 12), 0, iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); - } - } - case SF2RoundState_Escape: - { - // Initialize the escape timer, if needed. - if (g_iRoundEscapeTimeLimit > 0) - { - g_iRoundTime = g_iRoundEscapeTimeLimit; - g_hRoundTimer = CreateTimer(1.0, Timer_RoundTimeEscape, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hRoundTimer = INVALID_HANDLE; - } - - decl String:sName[32]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_logic_escape", false)) - { - AcceptEntityInput(ent, "FireUser1"); - break; - } - } - } - case SF2RoundState_Outro: - { - if (!g_bRoundHasEscapeObjective) - { - // Teleport winning players to the escape point. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - TeleportClientToEscapePoint(i); - } - } - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (IsClientInGhostMode(i)) - { - // Take the player out of ghost mode. - ClientSetGhostModeState(i, false); - TF2_RespawnPlayer(i); - } - else if (g_bPlayerProxy[i]) - { - TF2_RespawnPlayer(i); - } - - if (!g_bPlayerEliminated[i]) - { - // Give them back all their weapons so they can beat the crap out of the other team. - TF2_RegeneratePlayer(i); - } - - ClientUpdateListeningFlags(i); - } - } - } -} - -bool:IsRoundInEscapeObjective() -{ - return bool:(GetRoundState() == SF2RoundState_Escape); -} - -bool:IsRoundInWarmup() -{ - return bool:(GetRoundState() == SF2RoundState_Waiting); -} - -bool:IsRoundInIntro() -{ - return bool:(GetRoundState() == SF2RoundState_Intro); -} - -bool:IsRoundEnding() -{ - return bool:(GetRoundState() == SF2RoundState_Outro); -} - -bool:IsInfiniteBlinkEnabled() -{ - return bool:(g_bRoundInfiniteBlink || (GetConVarInt(g_cvPlayerInfiniteBlinkOverride) == 1)); -} - -bool:IsInfiniteFlashlightEnabled() -{ - return bool:(g_bRoundInfiniteFlashlight || (GetConVarInt(g_cvPlayerInfiniteFlashlightOverride) == 1)); -} - -bool:IsInfiniteSprintEnabled() -{ - return bool:(g_bRoundInfiniteSprint || (GetConVarInt(g_cvPlayerInfiniteSprintOverride) == 1)); -} - - -#define SF2_PLAYER_HUD_BLINK_SYMBOL "B" -#define SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL "ϟ" -#define SF2_PLAYER_HUD_BAR_SYMBOL "|" -#define SF2_PLAYER_HUD_BAR_MISSING_SYMBOL "" -#define SF2_PLAYER_HUD_INFINITY_SYMBOL "∞" -#define SF2_PLAYER_HUD_SPRINT_SYMBOL "»" - -public Action:Timer_ClientAverageUpdate(Handle:timer) -{ - if (timer != g_hClientAverageUpdateTimer) return Plugin_Stop; - - if (!g_bEnabled) return Plugin_Stop; - - if (IsRoundInWarmup() || IsRoundEnding()) return Plugin_Continue; - - // First, process through HUD stuff. - decl String:buffer[256]; - - static iHudColorHealthy[3] = { 150, 255, 150 }; - static iHudColorCritical[3] = { 255, 10, 10 }; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (IsPlayerAlive(i) && !IsClientInDeathCam(i)) - { - if (!g_bPlayerEliminated[i]) - { - if (DidClientEscape(i)) continue; - - new iMaxBars = 12; - new iBars = RoundToCeil(float(iMaxBars) * ClientGetBlinkMeter(i)); - if (iBars > iMaxBars) iBars = iMaxBars; - - Format(buffer, sizeof(buffer), "%s ", SF2_PLAYER_HUD_BLINK_SYMBOL); - - if (IsInfiniteBlinkEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - - if (!g_bSpecialRound || g_iSpecialRoundType != SPECIALROUND_LIGHTSOUT) - { - iBars = RoundToCeil(float(iMaxBars) * ClientGetFlashlightBatteryLife(i)); - if (iBars > iMaxBars) iBars = iMaxBars; - - decl String:sBuffer2[64]; - Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL); - StrCat(buffer, sizeof(buffer), sBuffer2); - - if (IsInfiniteFlashlightEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - } - - iBars = RoundToCeil(float(iMaxBars) * (float(ClientGetSprintPoints(i)) / 100.0)); - if (iBars > iMaxBars) iBars = iMaxBars; - - decl String:sBuffer2[64]; - Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_SPRINT_SYMBOL); - StrCat(buffer, sizeof(buffer), sBuffer2); - - if (IsInfiniteSprintEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - - - new Float:flHealthRatio = float(GetEntProp(i, Prop_Send, "m_iHealth")) / float(SDKCall(g_hSDKGetMaxHealth, i)); - - new iColor[3]; - for (new i2 = 0; i2 < 3; i2++) - { - iColor[i2] = RoundFloat(float(iHudColorHealthy[i2]) + (float(iHudColorCritical[i2] - iHudColorHealthy[i2]) * (1.0 - flHealthRatio))); - } - - SetHudTextParams(0.035, 0.83, - 0.3, - iColor[0], - iColor[1], - iColor[2], - 40, - _, - 1.0, - 0.07, - 0.5); - ShowSyncHudText(i, g_hHudSync2, buffer); - } - else - { - if (g_bPlayerProxy[i]) - { - new iMaxBars = 12; - new iBars = RoundToCeil(float(iMaxBars) * (float(g_iPlayerProxyControl[i]) / 100.0)); - if (iBars > iMaxBars) iBars = iMaxBars; - - strcopy(buffer, sizeof(buffer), "CONTROL\n"); - - for (new i2 = 0; i2 < iBars; i2++) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - - SetHudTextParams(-1.0, 0.83, - 0.3, - SF2_HUD_TEXT_COLOR_R, - SF2_HUD_TEXT_COLOR_G, - SF2_HUD_TEXT_COLOR_B, - 40, - _, - 1.0, - 0.07, - 0.5); - ShowSyncHudText(i, g_hHudSync2, buffer); - } - } - } - - ClientUpdateListeningFlags(i); - ClientUpdateMusicSystem(i); - } - - return Plugin_Continue; -} - -stock bool:IsClientParticipating(client) -{ - if (!IsValidClient(client)) return false; - - if (bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) - { - // Who would coach in this game? - return false; - } - - new iTeam = GetClientTeam(client); - - if (g_bPlayerLagCompensation[client]) - { - iTeam = g_iPlayerLagCompensationTeam[client]; - } - - switch (iTeam) - { - case TFTeam_Unassigned, TFTeam_Spectator: return false; - } - - if (_:TF2_GetPlayerClass(client) == 0) - { - // Player hasn't chosen a class? What. - return false; - } - - return true; -} - -Handle:GetQueueList() -{ - new Handle:hArray = CreateArray(3); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientParticipating(i)) continue; - if (IsPlayerGroupActive(ClientGetPlayerGroup(i))) continue; - - new index = PushArrayCell(hArray, i); - SetArrayCell(hArray, index, g_iPlayerQueuePoints[i], 1); - SetArrayCell(hArray, index, false, 2); - } - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - new index = PushArrayCell(hArray, i); - SetArrayCell(hArray, index, GetPlayerGroupQueuePoints(i), 1); - SetArrayCell(hArray, index, true, 2); - } - - if (GetArraySize(hArray)) SortADTArrayCustom(hArray, SortQueueList); - return hArray; -} - -SetClientPlayState(client, bool:bState, bool:bEnablePlay=true) -{ - if (bState) - { - if (!g_bPlayerEliminated[client]) return; - - g_bPlayerEliminated[client] = false; - g_bPlayerPlaying[client] = bEnablePlay; - g_hPlayerSwitchBlueTimer[client] = INVALID_HANDLE; - - ClientSetGhostModeState(client, false); - - PvP_SetPlayerPvPState(client, false, false, false); - - if (g_bSpecialRound) - { - SetClientPlaySpecialRoundState(client, true); - } - - if (g_bNewBossRound) - { - SetClientPlayNewBossRoundState(client, true); - } - - if (TF2_GetPlayerClass(client) == TFClassType:0) - { - // Player hasn't chosen a class for some reason. Choose one for him. - TF2_SetPlayerClass(client, TFClassType:GetRandomInt(1, 9), true, true); - } - - ChangeClientTeamNoSuicide(client, _:TFTeam_Red); - } - else - { - if (g_bPlayerEliminated[client]) return; - - g_bPlayerEliminated[client] = true; - g_bPlayerPlaying[client] = false; - - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - } -} - -SetClientPlayNewBossRoundState(client, bool:bState) -{ - g_bPlayerPlayedNewBossRound[client] = bState; -} - -SetClientPlaySpecialRoundState(client, bool:bState) -{ - g_bPlayerPlayedSpecialRound[client] = bState; -} - -TeleportClientToEscapePoint(client) -{ - if (!IsClientInGame(client)) return; - - new ent = EntRefToEntIndex(g_iRoundEscapePointEntity); - if (ent && ent != -1) - { - decl Float:flPos[3], Float:flAng[3]; - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); - GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); - - TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); - AcceptEntityInput(ent, "FireUser1", client); - } -} - -ForceInNextPlayersInQueue(iAmount, bool:bShowMessage=false) -{ - // Grab the next person in line, or the next group in line if space allows. - new iAmountLeft = iAmount; - new Handle:hPlayers = CreateArray(); - new Handle:hArray = GetQueueList(); - - for (new i = 0, iSize = GetArraySize(hArray); i < iSize && iAmountLeft > 0; i++) - { - if (!GetArrayCell(hArray, i, 2)) - { - new iClient = GetArrayCell(hArray, i); - if (g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; - - PushArrayCell(hPlayers, iClient); - iAmountLeft--; - } - else - { - new iGroupIndex = GetArrayCell(hArray, i); - if (!IsPlayerGroupActive(iGroupIndex)) continue; - - new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); - if (iMemberCount <= iAmountLeft) - { - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient) || g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; - if (ClientGetPlayerGroup(iClient) == iGroupIndex) - { - PushArrayCell(hPlayers, iClient); - } - } - - SetPlayerGroupPlaying(iGroupIndex, true); - - iAmountLeft -= iMemberCount; - } - } - } - - CloseHandle(hArray); - - for (new i = 0, iSize = GetArraySize(hPlayers); i < iSize; i++) - { - new iClient = GetArrayCell(hPlayers, i); - ClientSetQueuePoints(iClient, 0); - SetClientPlayState(iClient, true); - - if (bShowMessage) CPrintToChat(iClient, "%T", "SF2 Force Play", iClient); - } - - CloseHandle(hPlayers); -} - -public SortQueueList(index1, index2, Handle:array, Handle:hndl) -{ - new iQueuePoints1 = GetArrayCell(array, index1, 1); - new iQueuePoints2 = GetArrayCell(array, index2, 1); - - if (iQueuePoints1 > iQueuePoints2) return -1; - else if (iQueuePoints1 == iQueuePoints2) return 0; - return 1; -} - -// ========================================================== -// GENERIC PAGE/BOSS HOOKS AND FUNCTIONS -// ========================================================== - -public Action:Hook_SlenderObjectSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) - { - if (!IsValidEdict(GetEntPropEnt(other, Prop_Send, "m_hObserverTarget"))) return Plugin_Handled; - } - - return Plugin_Continue; -} - -public Action:Timer_SlenderBlinkBossThink(Handle:timer, any:entref) -{ - new slender = EntRefToEntIndex(entref); - if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return Plugin_Stop; - - if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (NPCGetType(iBossIndex) == SF2BossType_Creeper) - { - new bool:bMove = false; - - if ((GetGameTime() - g_flSlenderLastKill[iBossIndex]) >= GetProfileFloat(sProfile, "kill_cooldown")) - { - if (PeopleCanSeeSlender(iBossIndex, false, false) && !PeopleCanSeeSlender(iBossIndex, true, SlenderUsesBlink(iBossIndex))) - { - new iBestPlayer = -1; - new Handle:hArray = CreateArray(); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInDeathCam(i) || g_bPlayerEliminated[i] || DidClientEscape(i) || IsClientInGhostMode(i) || !PlayerCanSeeSlender(i, iBossIndex, false, false)) continue; - PushArrayCell(hArray, i); - } - - if (GetArraySize(hArray)) - { - decl Float:flSlenderPos[3]; - SlenderGetAbsOrigin(iBossIndex, flSlenderPos); - - decl Float:flTempPos[3]; - new iTempPlayer = -1; - new Float:flTempDist = 16384.0; - for (new i = 0; i < GetArraySize(hArray); i++) - { - new iClient = GetArrayCell(hArray, i); - GetClientAbsOrigin(iClient, flTempPos); - if (GetVectorDistance(flTempPos, flSlenderPos) < flTempDist) - { - iTempPlayer = iClient; - flTempDist = GetVectorDistance(flTempPos, flSlenderPos); - } - } - - iBestPlayer = iTempPlayer; - } - - CloseHandle(hArray); - - decl Float:buffer[3]; - if (iBestPlayer != -1 && SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, buffer)) - { - bMove = true; - - decl Float:flAng[3], Float:flBuffer[3]; - decl Float:flSlenderPos[3], Float:flPos[3]; - GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flSlenderPos); - GetClientAbsOrigin(iBestPlayer, flPos); - SubtractVectors(flPos, buffer, flAng); - GetVectorAngles(flAng, flAng); - - // Take care of angle offsets. - AddVectors(flAng, g_flSlenderEyeAngOffset[iBossIndex], flAng); - for (new i = 0; i < 3; i++) flAng[i] = AngleNormalize(flAng[i]); - - flAng[0] = 0.0; - - // Take care of position offsets. - GetProfileVector(sProfile, "pos_offset", flBuffer); - AddVectors(buffer, flBuffer, buffer); - - TeleportEntity(slender, buffer, flAng, NULL_VECTOR); - - new Float:flMaxRange = GetProfileFloat(sProfile, "teleport_range_max"); - new Float:flDist = GetVectorDistance(buffer, flPos); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - - if (flDist < (flMaxRange * 0.33)) - { - GetProfileString(sProfile, "model_closedist", sBuffer, sizeof(sBuffer)); - } - else if (flDist < (flMaxRange * 0.66)) - { - GetProfileString(sProfile, "model_averagedist", sBuffer, sizeof(sBuffer)); - } - else - { - GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); - } - - // Fallback if error. - if (!sBuffer[0]) GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); - - SetEntProp(slender, Prop_Send, "m_nModelIndex", PrecacheModel(sBuffer)); - - if (flDist <= NPCGetInstantKillRadius(iBossIndex)) - { - if (NPCGetFlags(iBossIndex) & SFF_FAKE) - { - SlenderMarkAsFake(iBossIndex); - return Plugin_Stop; - } - else - { - g_flSlenderLastKill[iBossIndex] = GetGameTime(); - ClientStartDeathCam(iBestPlayer, iBossIndex, buffer); - } - } - } - } - } - - if (bMove) - { - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_move_single", sBuffer, sizeof(sBuffer)); - if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - - GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); - if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING, SND_CHANGEVOL); - } - else - { - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); - if (sBuffer[0]) StopSound(slender, SNDCHAN_AUTO, sBuffer); - } - } - - return Plugin_Continue; -} - - -SlenderOnClientStressUpdate(client) -{ - new Float:flStress = g_flPlayerStress[client]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - new iBossFlags = NPCGetFlags(iBossIndex); - if (iBossFlags & SFF_MARKEDASFAKE || - iBossFlags & SFF_NOTELEPORT) - { - continue; - } - - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); - if (iTeleportTarget && iTeleportTarget != INVALID_ENT_REFERENCE) - { - if (g_bPlayerEliminated[iTeleportTarget] || - DidClientEscape(iTeleportTarget) || - flStress >= g_flSlenderTeleportMaxTargetStress[iBossIndex] || - GetGameTime() >= g_flSlenderTeleportMaxTargetTime[iBossIndex]) - { - // Queue for a new target and mark the old target in the rest period. - new Float:flRestPeriod = GetProfileFloat(sProfile, "teleport_target_rest_period", 15.0); - flRestPeriod = (flRestPeriod * GetRandomFloat(0.92, 1.08)) / (NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier); - - g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderTeleportPlayersRestTime[iBossIndex][iTeleportTarget] = GetGameTime() + flRestPeriod; - g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; - g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: lost target, putting at rest period", iBossIndex); -#endif - } - } - else if (!g_bRoundGrace) - { - new iPreferredTeleportTarget = INVALID_ENT_REFERENCE; - - new Float:flTargetStressMin = GetProfileFloat(sProfile, "teleport_target_stress_min", 0.2); - new Float:flTargetStressMax = GetProfileFloat(sProfile, "teleport_target_stress_max", 0.9); - - new Float:flTargetStress = flTargetStressMax - ((flTargetStressMax - flTargetStressMin) / (g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex))); - - new Float:flPreferredTeleportTargetStress = flTargetStress; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - IsClientInGhostMode(i) || - DidClientEscape(i)) - { - continue; - } - - if (g_flPlayerStress[i] < flPreferredTeleportTargetStress) - { - if (g_flSlenderTeleportPlayersRestTime[iBossIndex][i] <= GetGameTime()) - { - iPreferredTeleportTarget = i; - flPreferredTeleportTargetStress = g_flPlayerStress[i]; - } - } - } - - if (iPreferredTeleportTarget && iPreferredTeleportTarget != INVALID_ENT_REFERENCE) - { - // Set our preferred target to the new guy. - new Float:flTargetDuration = GetProfileFloat(sProfile, "teleport_target_persistency_period", 13.0); - new Float:flDeviation = GetRandomFloat(0.92, 1.08); - flTargetDuration = Pow(flDeviation * flTargetDuration, ((g_flRoundDifficultyModifier * (NPCGetAnger(iBossIndex) - 1.0)) / 2.0)) + ((flDeviation * flTargetDuration) - 1.0); - - g_iSlenderTeleportTarget[iBossIndex] = EntIndexToEntRef(iPreferredTeleportTarget); - g_flSlenderTeleportPlayersRestTime[iBossIndex][iPreferredTeleportTarget] = -1.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = GetGameTime() + flTargetDuration; - g_flSlenderTeleportTargetTime[iBossIndex] = GetGameTime(); - g_flSlenderTeleportMaxTargetStress[iBossIndex] = flTargetStress; - - iTeleportTarget = iPreferredTeleportTarget; - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: got new target %N", iBossIndex, iPreferredTeleportTarget); -#endif - } - } - } -} - -static GetPageMusicRanges() -{ - ClearArray(g_hPageMusicRanges); - - decl String:sName[64]; - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (sName[0] && !StrContains(sName, "sf2_page_music_", false)) - { - ReplaceString(sName, sizeof(sName), "sf2_page_music_", "", false); - - new String:sPageRanges[2][32]; - ExplodeString(sName, "-", sPageRanges, 2, 32); - - new iIndex = PushArrayCell(g_hPageMusicRanges, EntIndexToEntRef(ent)); - if (iIndex != -1) - { - new iMin = StringToInt(sPageRanges[0]); - new iMax = StringToInt(sPageRanges[0]); - -#if defined DEBUG - DebugMessage("Page range found: entity %d, iMin = %d, iMax = %d", ent, iMin, iMax); -#endif - SetArrayCell(g_hPageMusicRanges, iIndex, iMin, 1); - SetArrayCell(g_hPageMusicRanges, iIndex, iMax, 2); - } - } - } - - // precache - if (GetArraySize(g_hPageMusicRanges) > 0) - { - decl String:sPath[PLATFORM_MAX_PATH]; - - for (new i = 0; i < GetArraySize(g_hPageMusicRanges); i++) - { - ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - PrecacheSound(sPath); - } - } - } - - LogSF2Message("Loaded page music ranges successfully!"); -} - -SetPageCount(iNum) -{ - if (iNum > g_iPageMax) iNum = g_iPageMax; - - new iOldPageCount = g_iPageCount; - g_iPageCount = iNum; - - if (g_iPageCount != iOldPageCount) - { - if (g_iPageCount > iOldPageCount) - { - if (g_hRoundGraceTimer != INVALID_HANDLE) - { - TriggerTimer(g_hRoundGraceTimer); - } - - g_iRoundTime += g_iRoundTimeGainFromPage; - if (g_iRoundTime > g_iRoundTimeLimit) g_iRoundTime = g_iRoundTimeLimit; - - // Increase anger on selected bosses. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new Float:flPageDiff = NPCGetAngerAddOnPageGrabTimeDiff(i); - if (flPageDiff >= 0.0) - { - new iDiff = g_iPageCount - iOldPageCount; - if ((GetGameTime() - g_flPageFoundLastTime) < flPageDiff) - { - NPCAddAnger(i, NPCGetAngerAddOnPageGrab(i) * float(iDiff)); - } - } - } - - g_flPageFoundLastTime = GetGameTime(); - } - - // Notify logic entities. - decl String:sTargetName[64]; - decl String:sFindTargetName[64]; - Format(sFindTargetName, sizeof(sFindTargetName), "sf2_onpagecount_%d", g_iPageCount); - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); - if (sTargetName[0] && StrEqual(sTargetName, sFindTargetName, false)) - { - AcceptEntityInput(ent, "Trigger"); - break; - } - } - - new iClients[MAXPLAYERS + 1] = { -1, ... }; - new iClientsNum = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (!g_bPlayerEliminated[i] || IsClientInGhostMode(i)) - { - if (g_iPageCount) - { - iClients[iClientsNum] = i; - iClientsNum++; - } - } - } - - if (g_iPageCount > 0 && g_bRoundHasEscapeObjective && g_iPageCount == g_iPageMax) - { - // Escape initialized! - SetRoundState(SF2RoundState_Escape); - - if (iClientsNum) - { - new iGameTextEscape = GetTextEntity("sf2_escape_message", false); - if (iGameTextEscape != -1) - { - // Custom escape message. - decl String:sMessage[512]; - GetEntPropString(iGameTextEscape, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextEscape, g_hHudSync, sMessage); - } - else - { - // Default escape message. - for (new i = 0; i < iClientsNum; i++) - { - new client = iClients[i]; - ClientShowMainMessage(client, "%d/%d\n%T", g_iPageCount, g_iPageMax, "SF2 Default Escape Message", i); - } - } - } - } - else - { - if (iClientsNum) - { - new iGameTextPage = GetTextEntity("sf2_page_message", false); - if (iGameTextPage != -1) - { - // Custom page message. - decl String:sMessage[512]; - GetEntPropString(iGameTextPage, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextPage, g_hHudSync, sMessage, g_iPageCount, g_iPageMax); - } - else - { - // Default page message. - for (new i = 0; i < iClientsNum; i++) - { - new client = iClients[i]; - ClientShowMainMessage(client, "%d/%d", g_iPageCount, g_iPageMax); - } - } - } - } - - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - } -} - -GetTextEntity(const String:sTargetName[], bool:bCaseSensitive=true) -{ - // Try to see if we can use a custom message instead of the default. - decl String:targetName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "game_text")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (StrEqual(targetName, sTargetName, bCaseSensitive)) - { - return ent; - } - } - } - - return -1; -} - -ShowHudTextUsingTextEntity(const iClients[], iClientsNum, iGameText, Handle:hHudSync, const String:sMessage[], ...) -{ - if (!sMessage[0]) return; - if (!IsValidEntity(iGameText)) return; - - decl String:sTrueMessage[512]; - VFormat(sTrueMessage, sizeof(sTrueMessage), sMessage, 6); - - new Float:flX = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.x"); - new Float:flY = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.y"); - new iEffect = GetEntProp(iGameText, Prop_Data, "m_textParms.effect"); - new Float:flFadeInTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime"); - new Float:flFadeOutTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime"); - new Float:flHoldTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); - new Float:flFxTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fxTime"); - - new Color1[4] = { 255, 255, 255, 255 }; - new Color2[4] = { 255, 255, 255, 255 }; - - new iParmsOffset = FindDataMapOffs(iGameText, "m_textParms"); - if (iParmsOffset != -1) - { - // hudtextparms_s m_textParms - - Color1[0] = GetEntData(iGameText, iParmsOffset + 12, 1); - Color1[1] = GetEntData(iGameText, iParmsOffset + 13, 1); - Color1[2] = GetEntData(iGameText, iParmsOffset + 14, 1); - Color1[3] = GetEntData(iGameText, iParmsOffset + 15, 1); - - Color2[0] = GetEntData(iGameText, iParmsOffset + 16, 1); - Color2[1] = GetEntData(iGameText, iParmsOffset + 17, 1); - Color2[2] = GetEntData(iGameText, iParmsOffset + 18, 1); - Color2[3] = GetEntData(iGameText, iParmsOffset + 19, 1); - } - - SetHudTextParamsEx(flX, flY, flHoldTime, Color1, Color2, iEffect, flFxTime, flFadeInTime, flFadeOutTime); - - for (new i = 0; i < iClientsNum; i++) - { - new iClient = iClients[i]; - if (!IsValidClient(iClient) || IsFakeClient(iClient)) continue; - - ShowSyncHudText(iClient, hHudSync, sTrueMessage); - } -} - -// ========================================================== -// EVENT HOOKS -// ========================================================== - -public Event_RoundStart(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundStart"); -#endif - - // Reset some global variables. - g_iRoundCount++; - g_hRoundTimer = INVALID_HANDLE; - - SetRoundState(SF2RoundState_Invalid); - - SetPageCount(0); - g_iPageMax = 0; - g_flPageFoundLastTime = GetGameTime(); - - g_hVoteTimer = INVALID_HANDLE; - - // Remove all bosses from the game. - NPCRemoveAll(); - - // Refresh groups. - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - SetPlayerGroupPlaying(i, false); - CheckPlayerGroup(i); - } - - // Refresh players. - for (new i = 1; i <= MaxClients; i++) - { - ClientSetGhostModeState(i, false); - - g_bPlayerPlaying[i] = false; - g_bPlayerEliminated[i] = true; - g_bPlayerEscaped[i] = false; - } - - // Calculate the new round state. - if (g_bRoundWaitingForPlayers) - { - SetRoundState(SF2RoundState_Waiting); - } - else if (GetConVarBool(g_cvWarmupRound) && g_iRoundWarmupRoundCount < GetConVarInt(g_cvWarmupRoundNum)) - { - g_iRoundWarmupRoundCount++; - - SetRoundState(SF2RoundState_Waiting); - - ServerCommand("mp_restartgame 15"); - PrintCenterTextAll("Round restarting in 15 seconds"); - } - else - { - g_iRoundActiveCount++; - - InitializeNewGame(); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundStart"); -#endif -} - -public Event_RoundEnd(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundEnd"); -#endif - - SetRoundState(SF2RoundState_Outro); - - DistributeQueuePointsToPlayers(); - - g_iRoundEndCount++; - CheckRoundLimitForBossPackVote(g_iRoundEndCount); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundEnd"); -#endif -} - -static DistributeQueuePointsToPlayers() -{ - // Give away queue points. - new iDefaultAmount = 5; - new iAmount = iDefaultAmount; - new iAmount2 = iAmount; - new Action:iAction = Plugin_Continue; - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - - if (IsPlayerGroupPlaying(i)) - { - SetPlayerGroupQueuePoints(i, 0); - } - else - { - iAmount = iDefaultAmount; - iAmount2 = iAmount; - iAction = Plugin_Continue; - - Call_StartForward(fOnGroupGiveQueuePoints); - Call_PushCell(i); - Call_PushCellRef(iAmount2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) iAmount = iAmount2; - - SetPlayerGroupQueuePoints(i, GetPlayerGroupQueuePoints(i) + iAmount); - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient)) continue; - if (ClientGetPlayerGroup(iClient) == i) - { - CPrintToChat(iClient, "%T", "SF2 Give Group Queue Points", iClient, iAmount); - } - } - } - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (g_bPlayerPlaying[i]) - { - ClientSetQueuePoints(i, 0); - } - else - { - if (!IsClientParticipating(i)) - { - CPrintToChat(i, "%T", "SF2 No Queue Points To Spectator", i); - } - else - { - iAmount = iDefaultAmount; - iAmount2 = iAmount; - iAction = Plugin_Continue; - - Call_StartForward(fOnClientGiveQueuePoints); - Call_PushCell(i); - Call_PushCellRef(iAmount2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) iAmount = iAmount2; - - ClientSetQueuePoints(i, g_iPlayerQueuePoints[i] + iAmount); - CPrintToChat(i, "%T", "SF2 Give Queue Points", i, iAmount); - } - } - } -} - -public Action:Event_PlayerTeamPre(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerTeamPre"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - if (GetEventInt(event, "team") > 1 || GetEventInt(event, "oldteam") > 1) SetEventBroadcast(event, true); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerTeamPre"); -#endif - - return Plugin_Continue; -} - -public Event_PlayerTeam(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerTeam"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - new iNewTeam = GetEventInt(event, "team"); - if (iNewTeam <= _:TFTeam_Spectator) - { - if (g_bRoundGrace) - { - if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) - { - ForceInNextPlayersInQueue(1, true); - } - } - - // You're not playing anymore. - if (g_bPlayerPlaying[client]) - { - ClientSetQueuePoints(client, 0); - } - - g_bPlayerPlaying[client] = false; - g_bPlayerEliminated[client] = true; - g_bPlayerEscaped[client] = false; - - ClientSetGhostModeState(client, false); - - if (!bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) - { - // This is to prevent player spawn spam when someone is coaching. Who coaches in SF2, anyway? - TF2_RespawnPlayer(client); - } - - // Special round. - if (g_bSpecialRound) g_bPlayerPlayedSpecialRound[client] = true; - - // Boss round. - if (g_bNewBossRound) g_bPlayerPlayedNewBossRound[client] = true; - } - else - { - if (!g_bPlayerChoseTeam[client]) - { - g_bPlayerChoseTeam[client] = true; - - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - EmitSoundToClient(client, SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); - CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Projected{olive}."); - } - else - { - CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Normal{olive}."); - } - - CreateTimer(5.0, Timer_WelcomeMessage, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - } - - // Check groups. - if (!IsRoundEnding()) - { - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - CheckPlayerGroup(i); - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerTeam"); -#endif - -} - -/** - * Sets the player to the correct team if needed. Returns true if a change was necessary, false if no change occurred. - */ -static bool:HandlePlayerTeam(client, bool:bRespawn=true) -{ - if (!IsClientInGame(client) || !IsClientParticipating(client)) return false; - - if (!g_bPlayerEliminated[client]) - { - if (GetClientTeam(client) != _:TFTeam_Red) - { - if (bRespawn) - ChangeClientTeamNoSuicide(client, _:TFTeam_Red); - else - ChangeClientTeam(client, _:TFTeam_Red); - - return true; - } - } - else - { - if (GetClientTeam(client) != _:TFTeam_Blue) - { - if (bRespawn) { - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - } - else - ChangeClientTeam(client, _:TFTeam_Blue); - return true; - } - } - - return false; -} - -static HandlePlayerIntroState(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client) || !IsClientParticipating(client)) return; - - if (!IsRoundInIntro()) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START HandlePlayerIntroState(%d)", client); -#endif - - // Disable movement on player. - SetEntityFlags(client, GetEntityFlags(client) | FL_FROZEN); - - new Float:flDelay = 0.0; - if (!IsFakeClient(client)) - { - flDelay = GetClientLatency(client, NetFlow_Outgoing); - } - - CreateTimer(flDelay * 4.0, Timer_IntroBlackOut, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END HandlePlayerIntroState(%d)", client); -#endif -} - -HandlePlayerHUD(client) -{ - if (IsRoundInWarmup() || IsClientInGhostMode(client)) - { - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - else - { - if (!g_bPlayerEliminated[client]) - { - if (!DidClientEscape(client)) - { - // Player is in the game; disable normal HUD. - SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); - } - else - { - // Player isn't in the game; enable normal HUD behavior. - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - } - else - { - if (g_bPlayerProxy[client]) - { - // Player is in the game; disable normal HUD. - SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); - } - else - { - // Player isn't in the game; enable normal HUD behavior. - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - } - } -} - -public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client <= 0) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerSpawn(%d)", client); -#endif - - if (!IsClientParticipating(client)) - { - ClientSetGhostModeState(client, false); - } - - g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; - - g_hOverlayUpdateTimer[client] = INVALID_HANDLE; - g_iGhostNextHelpPhrase[client] = 0; - - if (IsPlayerAlive(client) && IsClientParticipating(client)) - { - if (HandlePlayerTeam(client)) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("client->HandlePlayerTeam()"); -#endif - } - else - { - g_iPlayerPageCount[client] = 0; - - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientUpdateListeningFlags(client); - ClientUpdateMusicSystem(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - ClientResetProxy(client); - ClientResetHints(client); - ClientResetScare(client); - - ClientResetDeathCam(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientHandleGhostMode(client); - - if (!g_bPlayerEliminated[client]) - { - ClientStartDrainingBlinkMeter(client); - ClientSetScareBoostEndTime(client, -1.0); - - ClientStartCampingTimer(client); - - HandlePlayerIntroState(client); - - // screen overlay timer - g_hPlayerOverlayCheck[client] = CreateTimer(0.0, Timer_PlayerOverlayCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerOverlayCheck[client], true); - - if (DidClientEscape(client)) - { - CreateTimer(0.1, Timer_TeleportPlayerToEscapePoint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - ClientEnableConstantGlow(client, "head"); - ClientActivateUltravision(client); - CreateTimer(0.1, Timer_CreateSpriteOverlay, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - else - { - g_hPlayerOverlayCheck[client] = INVALID_HANDLE; - } - - g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - HandlePlayerHUD(client); - } - } - - PvP_OnPlayerSpawn(client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerSpawn(%d)", client); -#endif -} - -public Action:Timer_UpdateOverlayTime(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - new String:Time[64], String:DigitBuffer[2]; - FormatTime(Time, sizeof(Time), "%d"); // Day - Format(DigitBuffer, 2, "%s", Time[0]); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit0), StringToFloat(DigitBuffer)); - Format(DigitBuffer, 2, "%s", Time[1]); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit1), StringToFloat(DigitBuffer)); - FormatTime(Time, sizeof(Time), "%m"); // Month - Format(DigitBuffer, 2, "%s", Time[0]); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit3), StringToFloat(DigitBuffer)); - Format(DigitBuffer, 2, "%s", Time[1]); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit4), StringToFloat(DigitBuffer)); - FormatTime(Time, sizeof(Time), "%H"); // Hours - Format(DigitBuffer, 2, "%s", Time[0]); - Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit0), StringToFloat(DigitBuffer)); - Format(DigitBuffer, 2, "%s", Time[1]); - Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit1), StringToFloat(DigitBuffer)); - FormatTime(Time, sizeof(Time), "%M"); // Minutes - Format(DigitBuffer, 2, "%s", Time[0]); - Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit3), StringToFloat(DigitBuffer)); - Format(DigitBuffer, 2, "%s", Time[1]); - Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit4), StringToFloat(DigitBuffer)); - if(g_hRoundTimer != INVALID_HANDLE) { - //g_iRoundTime - //g_iRoundTime = g_iRoundTimeLimit;g_iPageCount == g_iPageMax - new time = g_iRoundTime; - decl maxTime; - if(g_iPageCount == g_iPageMax) { - maxTime = g_iRoundEscapeTimeLimit; - } else { - maxTime = g_iRoundTimeLimit; - } - if(float(time) / float(maxTime) >= 0.75) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 0.0); - else if(float(time) / float(maxTime) >= 0.5) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 1.0); - else if(float(time) / float(maxTime) >= 0.25) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 2.0); - else if(float(time) / float(maxTime) >= 0.17) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); - else { - Overlay_Color_Red(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[0]); - Overlay_Color_Green(OverlayRef_ByLayer(client, _:HudBattery), 0.0); - Overlay_Color_Blue(OverlayRef_ByLayer(client, _:HudBattery), 0.0); - if(g_bHudBatteryToggle[client]) Overlay_Hide(OverlayRef_ByLayer(client, _:HudBattery));// Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); - else { - Overlay_Show(OverlayRef_ByLayer(client, _:HudBattery)); - Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); - } - //PrintToServer("%d", g_bHudBatteryToggle[client]); - g_bHudBatteryToggle[client] = !g_bHudBatteryToggle[client]; - } - //Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), float(g_iRoundTime)); - /* - Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 0.0); - Overlay_Color_Red(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[0]); - Overlay_Color_Green(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[1]); - Overlay_Color_Blue(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[1]); - */ - } -} - -public Action:Timer_CreateSpriteOverlay(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - /* - PrintToServer("Add_Sprite()"); - Add_Sprite(client, OverlayGeneric, 14.0); - //Overlay_Render(client, _:OverlayGeneric, g_fOverlayMatOffset[OverlayGeneric], g_sOverlayMat[OverlayGeneric], -5.0); - new spr; - new String:cl[32]; - PrintToServer("Overlay_Layer_Exists(client, _:OverlayGeneric): %d", Overlay_Layer_Exists(client, _:OverlayGeneric, spr)); - Overlay_Show(spr); - GetEntityClassname(spr, cl, sizeof(cl)); - PrintToServer("spr: %d (%s)", spr, cl); - */ - g_fOverlayMatRotation[OverlayGeneric][2] = GetRandomFloat(0.0, 180.0); - Add_Sprite(client, OverlayGeneric, 35.0); - Add_Sprite(client, Crosshair, 3.0); - - Add_Sprite(client, DateDigit0, g_fHudDigitScale); - Add_Sprite(client, DateDigit1, g_fHudDigitScale); - Add_Sprite(client, DateDigit2, g_fHudDigitScale); - Add_Sprite(client, DateDigit3, g_fHudDigitScale); - Add_Sprite(client, DateDigit4, g_fHudDigitScale); - Add_Sprite(client, DateDigit5, g_fHudDigitScale); - Add_Sprite(client, DateDigit6, g_fHudDigitScale); - Add_Sprite(client, DateDigit7, g_fHudDigitScale); - Add_Sprite(client, ClockDigit0, g_fHudDigitScale); - Add_Sprite(client, ClockDigit1, g_fHudDigitScale); - Add_Sprite(client, ClockDigit2, g_fHudDigitScale); - Add_Sprite(client, ClockDigit3, g_fHudDigitScale); - Add_Sprite(client, ClockDigit4, g_fHudDigitScale); - - Add_Sprite(client, HudRec, g_fHudDigitScale); - Add_Sprite(client, HudBattery, g_fHudDigitScale); - g_bHudBatteryToggle[client] = true; - //PrintToServer("%d", Overlay_Exists(OverlayRef_ByLayer(client, _:DateDigit0))); - for(new i = _:DateDigit0; i <= _:HudBattery; i++) { - Overlay_Framerate(OverlayRef_ByLayer(client, i), 0.0); - Overlay_Color_Red(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[0]); - Overlay_Color_Green(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[1]); - Overlay_Color_Blue(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[1]); - } - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit2), 10.0); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit5), 10.0); - Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit2), 11.0); - /* - new Float:yearDigits[2], String:buffer[16]; - Format(buffer, 16, "0000%s", g_sCameraYear); - strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); - Format(buffer, 1, "%s", g_sCameraYear[strlen(g_sCameraYear)-1]); - strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); - yearDigits[0] = StringToFloat(buffer[0]); - Format(buffer, 1, "%s", g_sCameraYear[strlen(g_sCameraYear)-1]); - strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); - yearDigits[1] = StringToFloat(buffer[0]); - */ - //PrintToServer("%s %d %s %s %s", g_sCameraYear, strlen(g_sCameraYear), g_sCameraYear[strlen(g_sCameraYear)-1], g_sCameraYear[strlen(g_sCameraYear)-2], g_sCameraYear[strlen(g_sCameraYear)-3]); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit6), 8.0); - Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit7), 6.0); - g_hOverlayUpdateTimer[client] = CreateTimer(1.2, Timer_UpdateOverlayTime, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); - //Hide_Weapon(client); -} - -public Action:Timer_IntroBlackOut(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!IsRoundInIntro()) return; - - if (!IsPlayerAlive(client) || g_bPlayerEliminated[client]) return; - - // Black out the player's screen. - new iFadeFlags = FFADE_OUT | FFADE_STAYOUT | FFADE_PURGE; - UTIL_ScreenFade(client, 0, FixedUnsigned16(90.0, 1 << 12), iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); -} - -public Event_PostInventoryApplication(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PostInventoryApplication"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PostInventoryApplication"); -#endif -} - -public Action:Event_DontBroadcastToClients(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsRoundInWarmup()) return Plugin_Continue; - - SetEventBroadcast(event, true); - return Plugin_Continue; -} - -public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerDeathPre"); -#endif - - if (!IsRoundInWarmup()) - { - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - if (!IsRoundEnding()) - { - if (g_bRoundGrace || g_bPlayerEliminated[client] || IsClientInGhostMode(client)) - { - SetEventBroadcast(event, true); - } - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerDeathPre"); -#endif - - return Plugin_Continue; -} - -public Event_PlayerHurt(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client <= 0) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerHurt"); -#endif - - ClientDisableFakeLagCompensation(client); - - new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); - if (attacker > 0) - { - if (g_bPlayerProxy[attacker]) - { - g_iPlayerProxyControl[attacker] = 100; - } - } - - // Play any sounds, if any. - if (g_bPlayerProxy[client]) - { - new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iProxyMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - if (GetRandomStringFromProfile(sProfile, "sound_proxy_hurt", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_hurt_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_hurt_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_hurt_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_hurt_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_hurt_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerHurt"); -#endif -} - -public Event_PlayerDeath(Handle:event, const String:name[], bool:dB) -{ - new client = GetClientOfUserId(GetEventInt(event, "userid")); - - if (client <= 0) return; - - DestroySpriteOverlay(client); - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerDeath(%d)", client); -#endif - - new bool:bFake = bool:(GetEventInt(event, "death_flags") & TF_DEATHFLAG_DEADRINGER); - new inflictor = GetEventInt(event, "inflictor_entindex"); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("inflictor = %d", inflictor); -#endif - - if (!bFake) - { - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetDeathCam(client); - - ClientUpdateMusicSystem(client); - - PvP_SetPlayerPvPState(client, false, false, false); - - if (IsRoundInWarmup()) - { - CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - if (!g_bPlayerEliminated[client]) - { - if (IsRoundInIntro() || g_bRoundGrace || DidClientEscape(client)) - { - CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_bPlayerEliminated[client] = true; - g_bPlayerEscaped[client] = false; - g_hPlayerSwitchBlueTimer[client] = CreateTimer(2.5, Timer_PlayerSwitchToBlue, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - ClientCommand(client, "r_screenoverlay %s", STATIC_OVERLAY); - EmitSoundToClient(client, STATIC_SOUND, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - else - { - } - - { - // If this player was killed by a boss, play a sound. - new npcIndex = NPCGetFromEntIndex(inflictor); - if (npcIndex != -1) - { - decl String:npcProfile[SF2_MAX_PROFILE_NAME_LENGTH], String:buffer[PLATFORM_MAX_PATH]; - NPCGetProfile(npcIndex, npcProfile, sizeof(npcProfile)); - - if (GetRandomStringFromProfile(npcProfile, "sound_attack_killed_all", buffer, sizeof(buffer)) && strlen(buffer) > 0) - { - if (!g_bPlayerEliminated[client]) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_HELICOPTER); - } - } - - SlenderPerformVoice(npcIndex, "sound_attack_killed"); - } - } - - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - - // Notify to other bosses that this player has died. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (EntRefToEntIndex(g_iSlenderTarget[i]) == client) - { - g_iSlenderInterruptConditions[i] |= COND_CHASETARGETINVALIDATED; - GetClientAbsOrigin(client, g_flSlenderChaseDeathPosition[i]); - } - } - } - - if (g_bPlayerProxy[client]) - { - // We're a proxy, so play some sounds. - - new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iProxyMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - if (GetRandomStringFromProfile(sProfile, "sound_proxy_death", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_death_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_death_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_death_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_death_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_death_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } - - ClientResetProxy(client, false); - ClientUpdateListeningFlags(client); - - // Half-Zatoichi nerf code. - new iKatanaHealthGain = GetConVarInt(g_cvHalfZatoichiHealthGain); - if (iKatanaHealthGain >= 0) - { - new iAttacker = GetClientOfUserId(GetEventInt(event, "attacker")); - if (iAttacker > 0) - { - if (!IsClientInPvP(iAttacker) && (!g_bPlayerEliminated[iAttacker] || g_bPlayerProxy[iAttacker])) - { - decl String:sWeapon[64]; - GetEventString(event, "weapon", sWeapon, sizeof(sWeapon)); - - if (StrEqual(sWeapon, "demokatana")) - { - new iAttackerPreHealth = GetEntProp(iAttacker, Prop_Send, "m_iHealth"); - new Handle:hPack = CreateDataPack(); - WritePackCell(hPack, GetClientUserId(iAttacker)); - WritePackCell(hPack, iAttackerPreHealth + iKatanaHealthGain); - - CreateTimer(0.0, Timer_SetPlayerHealth, hPack, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - } - - g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; - } - - PvP_OnPlayerDeath(client, bFake); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerDeath(%d)", client); -#endif -} - -public Action:Timer_SetPlayerHealth(Handle:timer, any:data) -{ - new Handle:hPack = Handle:data; - ResetPack(hPack); - new iAttacker = GetClientOfUserId(ReadPackCell(hPack)); - new iHealth = ReadPackCell(hPack); - CloseHandle(hPack); - - if (iAttacker <= 0) return; - - SetEntProp(iAttacker, Prop_Data, "m_iHealth", iHealth); - SetEntProp(iAttacker, Prop_Send, "m_iHealth", iHealth); -} - -public Action:Timer_PlayerSwitchToBlue(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSwitchBlueTimer[client]) return; - - ChangeClientTeam(client, _:TFTeam_Blue); -} - -public Action:Timer_RoundStart(Handle:timer) -{ - if (g_iPageMax > 0) - { - new Handle:hArrayClients = CreateArray(); - new iClients[MAXPLAYERS + 1]; - new iClientsNum = 0; - - new iGameText = GetTextEntity("sf2_intro_message", false); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || g_bPlayerEliminated[i]) continue; - - if (iGameText == -1) - { - if (g_iPageMax > 1) - { - ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Plural", i, g_iPageMax); - } - else - { - ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Singular", i, g_iPageMax); - } - } - - PushArrayCell(hArrayClients, GetClientUserId(i)); - iClients[iClientsNum] = i; - iClientsNum++; - } - - // Show difficulty menu. - if (iClientsNum) - { - // Automatically set it to Normal. - SetConVarInt(g_cvDifficulty, Difficulty_Normal); - - g_hVoteTimer = CreateTimer(1.0, Timer_VoteDifficulty, hArrayClients, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hVoteTimer, true); - - if (iGameText != -1) - { - decl String:sMessage[512]; - GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); - } - } - else - { - CloseHandle(hArrayClients); - } - } -} - -public Action:Timer_CheckRoundWinConditions(Handle:timer) -{ - CheckRoundWinConditions(); -} - -public Action:Timer_RoundGrace(Handle:timer) -{ - if (timer != g_hRoundGraceTimer) return; - - g_bRoundGrace = false; - g_hRoundGraceTimer = INVALID_HANDLE; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientParticipating(i)) g_bPlayerEliminated[i] = true; - } - - // Initialize the main round timer. - if (g_iRoundTimeLimit > 0) - { - // Set round time. - g_iRoundTime = g_iRoundTimeLimit; - g_hRoundTimer = CreateTimer(1.0, Timer_RoundTime, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - else - { - // Infinite round time. - g_hRoundTimer = INVALID_HANDLE; - } - - CPrintToChatAll("{olive}%t", "SF2 Grace Period End"); -} - -public Action:Timer_RoundTime(Handle:timer) -{ - if (timer != g_hRoundTimer) return Plugin_Stop; - - if (g_iRoundTime <= 0) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; - - decl Float:flBuffer[3]; - GetClientAbsOrigin(i, flBuffer); - SDKHooks_TakeDamage(i, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - } - - return Plugin_Stop; - } - - g_iRoundTime--; - - new hours, minutes, seconds; - FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); - - SetHudTextParams(-1.0, 0.1, - 1.0, - SF2_HUD_TEXT_COLOR_R, SF2_HUD_TEXT_COLOR_G, SF2_HUD_TEXT_COLOR_B, SF2_HUD_TEXT_COLOR_A, - _, - _, - 1.5, 1.5); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; - ShowSyncHudText(i, g_hRoundTimerSync, "%d/%d\n%d:%02d", g_iPageCount, g_iPageMax, minutes, seconds); - } - - return Plugin_Continue; -} - -public Action:Timer_RoundTimeEscape(Handle:timer) -{ - if (timer != g_hRoundTimer) return Plugin_Stop; - - if (g_iRoundTime <= 0) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i) || DidClientEscape(i)) continue; - - decl Float:flBuffer[3]; - GetClientAbsOrigin(i, flBuffer); - ClientStartDeathCam(i, 0, flBuffer); - } - - return Plugin_Stop; - } - - new hours, minutes, seconds; - FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); - - SetHudTextParams(-1.0, 0.1, - 1.0, - SF2_HUD_TEXT_COLOR_R, - SF2_HUD_TEXT_COLOR_G, - SF2_HUD_TEXT_COLOR_B, - SF2_HUD_TEXT_COLOR_A, - _, - _, - 1.5, 1.5); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; - ShowSyncHudText(i, g_hRoundTimerSync, "%T\n%d:%02d", "SF2 Default Escape Message", i, minutes, seconds); - } - - g_iRoundTime--; - - return Plugin_Continue; -} - -public Action:Timer_VoteDifficulty(Handle:timer, any:data) -{ - new Handle:hArrayClients = Handle:data; - - if (timer != g_hVoteTimer || IsRoundEnding()) - { - CloseHandle(hArrayClients); - return Plugin_Stop; - } - - if (IsVoteInProgress()) return Plugin_Continue; // There's another vote in progess. Wait. - - new iClients[MAXPLAYERS + 1] = { -1, ... }; - new iClientsNum; - for (new i = 0, iSize = GetArraySize(hArrayClients); i < iSize; i++) - { - new iClient = GetClientOfUserId(GetArrayCell(hArrayClients, i)); - if (iClient <= 0) continue; - - iClients[iClientsNum] = iClient; - iClientsNum++; - } - - CloseHandle(hArrayClients); - - VoteMenu(g_hMenuVoteDifficulty, iClients, iClientsNum, 15); - - return Plugin_Stop; -} - -static InitializeMapEntities() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeMapEntities()"); -#endif - - g_bRoundInfiniteFlashlight = false; - g_bRoundInfiniteBlink = false; - g_bRoundInfiniteSprint = false; - g_bRoundHasEscapeObjective = false; - - g_iRoundTimeLimit = GetConVarInt(g_cvTimeLimit); - g_iRoundEscapeTimeLimit = GetConVarInt(g_cvTimeLimitEscape); - g_iRoundTimeGainFromPage = GetConVarInt(g_cvTimeGainFromPageGrab); - - // Reset page reference. - g_bPageRef = false; - strcopy(g_strPageRefModel, sizeof(g_strPageRefModel), ""); - g_flPageRefModelScale = 1.0; - - new Handle:hArray = CreateArray(2); - new Handle:hPageTrie = CreateTrie(); - - decl String:targetName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (!StrContains(targetName, "sf2_maxpages_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_maxpages_", "", false); - g_iPageMax = StringToInt(targetName); - } - else if (!StrContains(targetName, "sf2_page_spawnpoint", false)) - { - if (!StrContains(targetName, "sf2_page_spawnpoint_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_page_spawnpoint_", "", false); - if (targetName[0]) - { - new Handle:hButtStallion = INVALID_HANDLE; - if (!GetTrieValue(hPageTrie, targetName, hButtStallion)) - { - hButtStallion = CreateArray(); - SetTrieValue(hPageTrie, targetName, hButtStallion); - } - - new iIndex = FindValueInArray(hArray, hButtStallion); - if (iIndex == -1) - { - iIndex = PushArrayCell(hArray, hButtStallion); - } - - PushArrayCell(hButtStallion, ent); - SetArrayCell(hArray, iIndex, true, 1); - } - else - { - new iIndex = PushArrayCell(hArray, ent); - SetArrayCell(hArray, iIndex, false, 1); - } - } - else - { - new iIndex = PushArrayCell(hArray, ent); - SetArrayCell(hArray, iIndex, false, 1); - } - } - else if (!StrContains(targetName, "sf2_logic_escape", false)) - { - g_bRoundHasEscapeObjective = true; - } - else if (!StrContains(targetName, "sf2_infiniteflashlight", false)) - { - g_bRoundInfiniteFlashlight = true; - } - else if (!StrContains(targetName, "sf2_infiniteblink", false)) - { - g_bRoundInfiniteBlink = true; - } - else if (!StrContains(targetName, "sf2_infinitesprint", false)) - { - g_bRoundInfiniteSprint = true; - } - else if (!StrContains(targetName, "sf2_time_limit_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_time_limit_", "", false); - g_iRoundTimeLimit = StringToInt(targetName); - - LogSF2Message("Found sf2_time_limit entity, set time limit to %d", g_iRoundTimeLimit); - } - else if (!StrContains(targetName, "sf2_escape_time_limit_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_escape_time_limit_", "", false); - g_iRoundEscapeTimeLimit = StringToInt(targetName); - - LogSF2Message("Found sf2_escape_time_limit entity, set escape time limit to %d", g_iRoundEscapeTimeLimit); - } - else if (!StrContains(targetName, "sf2_time_gain_from_page_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_time_gain_from_page_", "", false); - g_iRoundTimeGainFromPage = StringToInt(targetName); - - LogSF2Message("Found sf2_time_gain_from_page entity, set time gain to %d", g_iRoundTimeGainFromPage); - } - else if (g_iRoundActiveCount == 1 && (!StrContains(targetName, "sf2_maxplayers_", false))) - { - ReplaceString(targetName, sizeof(targetName), "sf2_maxplayers_", "", false); - SetConVarInt(g_cvMaxPlayers, StringToInt(targetName)); - - LogSF2Message("Found sf2_maxplayers entity, set maxplayers to %d", StringToInt(targetName)); - } - else if (!StrContains(targetName, "sf2_boss_override_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_boss_override_", "", false); - SetConVarString(g_cvBossProfileOverride, targetName); - - LogSF2Message("Found sf2_boss_override entity, set boss profile override to %s", targetName); - } - } - } - - // Get a reference entity, if any. - - ent = -1; - while ((ent = FindEntityByClassname(ent, "prop_dynamic")) != -1) - { - if (g_bPageRef) break; - - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (StrEqual(targetName, "sf2_page_model", false)) - { - g_bPageRef = true; - GetEntPropString(ent, Prop_Data, "m_ModelName", g_strPageRefModel, sizeof(g_strPageRefModel)); - g_flPageRefModelScale = 1.0; - } - } - } - - new iPageCount = GetArraySize(hArray); - if (iPageCount) - { - SortADTArray(hArray, Sort_Random, Sort_Integer); - - decl Float:vecPos[3], Float:vecAng[3], Float:vecDir[3]; - decl page; - ent = -1; - - for (new i = 0; i < iPageCount && (i + 1) <= g_iPageMax; i++) - { - if (bool:GetArrayCell(hArray, i, 1)) - { - new Handle:hButtStallion = Handle:GetArrayCell(hArray, i); - ent = GetArrayCell(hButtStallion, GetRandomInt(0, GetArraySize(hButtStallion) - 1)); - } - else - { - ent = GetArrayCell(hArray, i); - } - - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", vecPos); - GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", vecAng); - GetAngleVectors(vecAng, vecDir, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(vecDir, vecDir); - ScaleVector(vecDir, 1.0); - - page = CreateEntityByName("prop_dynamic_override"); - if (page != -1) - { - TeleportEntity(page, vecPos, vecAng, NULL_VECTOR); - DispatchKeyValue(page, "targetname", "sf2_page"); - - if (g_bPageRef) - { - SetEntityModel(page, g_strPageRefModel); - } - else - { - SetEntityModel(page, PAGE_MODEL); - } - - DispatchKeyValue(page, "solid", "2"); - DispatchSpawn(page); - ActivateEntity(page); - SetVariantInt(i); - AcceptEntityInput(page, "Skin"); - AcceptEntityInput(page, "EnableCollision"); - - if (g_bPageRef) - { - SetEntPropFloat(page, Prop_Send, "m_flModelScale", g_flPageRefModelScale); - } - else - { - SetEntPropFloat(page, Prop_Send, "m_flModelScale", PAGE_MODELSCALE); - } - - SDKHook(page, SDKHook_OnTakeDamage, Hook_PageOnTakeDamage); - SDKHook(page, SDKHook_SetTransmit, Hook_SlenderObjectSetTransmit); - } - } - - // Safely remove all handles. - for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) - { - if (bool:GetArrayCell(hArray, i, 1)) - { - CloseHandle(Handle:GetArrayCell(hArray, i)); - } - } - - Call_StartForward(fOnPagesSpawned); - Call_Finish(); - } - - CloseHandle(hPageTrie); - CloseHandle(hArray); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeMapEntities()"); -#endif -} - -static HandleSpecialRoundState() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleSpecialRoundState()"); -#endif - - new bool:bOld = g_bSpecialRound; - new bool:bContinuousOld = g_bSpecialRoundContinuous; - g_bSpecialRound = false; - g_bSpecialRoundNew = false; - g_bSpecialRoundContinuous = false; - - new bool:bForceNew = false; - - if (bOld) - { - if (bContinuousOld) - { - // Check if there are players who haven't played the special round yet. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedSpecialRound[i] = true; - continue; - } - - if (!g_bPlayerPlayedSpecialRound[i]) - { - // Someone didn't get to play this yet. Continue the special round. - g_bSpecialRound = true; - g_bSpecialRoundContinuous = true; - break; - } - } - } - } - - new iRoundInterval = GetConVarInt(g_cvSpecialRoundInterval); - - if (iRoundInterval > 0 && g_iSpecialRoundCount >= iRoundInterval) - { - g_bSpecialRound = true; - bForceNew = true; - } - - // Do special round force override and reset it. - if (GetConVarInt(g_cvSpecialRoundForce) >= 0) - { - g_bSpecialRound = GetConVarBool(g_cvSpecialRoundForce); - SetConVarInt(g_cvSpecialRoundForce, -1); - } - - if (g_bSpecialRound) - { - if (bForceNew || !bOld || !bContinuousOld) - { - g_bSpecialRoundNew = true; - } - - if (g_bSpecialRoundNew) - { - if (GetConVarInt(g_cvSpecialRoundBehavior) == 1) - { - g_bSpecialRoundContinuous = true; - } - else - { - // New special round, but it's not continuous. - g_bSpecialRoundContinuous = false; - } - } - } - else - { - g_bSpecialRoundContinuous = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleSpecialRoundState() -> g_bSpecialRound = %d (count = %d, new = %d, continuous = %d)", g_bSpecialRound, g_iSpecialRoundCount, g_bSpecialRoundNew, g_bSpecialRoundContinuous); -#endif -} - -bool:IsNewBossRoundRunning() -{ - return g_bNewBossRound; -} - -/** - * Returns an array which contains all the profile names valid to be chosen for a new boss round. - */ -static Handle:GetNewBossRoundProfileList() -{ - new Handle:hBossList = CloneArray(GetSelectableBossProfileList()); - - if (GetArraySize(hBossList) > 0) - { - decl String:sMainBoss[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossMain, sMainBoss, sizeof(sMainBoss)); - - new index = FindStringInArray(hBossList, sMainBoss); - if (index != -1) - { - // Main boss exists; remove him from the list. - RemoveFromArray(hBossList, index); - } - else - { - // Main boss doesn't exist; remove the first boss from the list. - RemoveFromArray(hBossList, 0); - } - } - - return hBossList; -} - -static HandleNewBossRoundState() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleNewBossRoundState()"); -#endif - - new bool:bOld = g_bNewBossRound; - new bool:bContinuousOld = g_bNewBossRoundContinuous; - g_bNewBossRound = false; - g_bNewBossRoundNew = false; - g_bNewBossRoundContinuous = false; - - new bool:bForceNew = false; - - if (bOld) - { - if (bContinuousOld) - { - // Check if there are players who haven't played the boss round yet. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedNewBossRound[i] = true; - continue; - } - - if (!g_bPlayerPlayedNewBossRound[i]) - { - // Someone didn't get to play this yet. Continue the boss round. - g_bNewBossRound = true; - g_bNewBossRoundContinuous = true; - break; - } - } - } - } - - // Don't force a new special round while a continuous round is going on. - if (!g_bNewBossRoundContinuous) - { - new iRoundInterval = GetConVarInt(g_cvNewBossRoundInterval); - - if (/*iRoundInterval > 0 &&*/ iRoundInterval <= 0 || g_iNewBossRoundCount >= iRoundInterval) - { - g_bNewBossRound = true; - bForceNew = true; - } - } - - // Do boss round force override and reset it. - if (GetConVarInt(g_cvNewBossRoundForce) >= 0) - { - g_bNewBossRound = GetConVarBool(g_cvNewBossRoundForce); - SetConVarInt(g_cvNewBossRoundForce, -1); - } - - // Check if we have enough bosses. - if (g_bNewBossRound) - { - new Handle:hBossList = GetNewBossRoundProfileList(); - - if (GetArraySize(hBossList) < 1) - { - g_bNewBossRound = false; // Not enough bosses. - } - - CloseHandle(hBossList); - } - - if (g_bNewBossRound) - { - if (bForceNew || !bOld || !bContinuousOld) - { - g_bNewBossRoundNew = true; - } - - if (g_bNewBossRoundNew) - { - if (GetConVarInt(g_cvNewBossRoundBehavior) == 1) - { - g_bNewBossRoundContinuous = true; - } - else - { - // New "new boss round", but it's not continuous. - g_bNewBossRoundContinuous = false; - } - } - } - else - { - g_bNewBossRoundContinuous = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleNewBossRoundState() -> g_bNewBossRound = %d (count = %d, new = %d, continuous = %d)", g_bNewBossRound, g_iNewBossRoundCount, g_bNewBossRoundNew, g_bNewBossRoundContinuous); -#endif -} - -/** - * Returns the amount of players that are in game and currently not eliminated. - */ -stock GetActivePlayerCount() -{ - new count = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - count++; - } - } - - return count; -} - -static SelectStartingBossesForRound() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START SelectStartingBossesForRound()"); -#endif - - new Handle:hSelectableBossList = GetSelectableBossProfileList(); - - // Select which boss profile to use. - decl String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); - - if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) - { - // Pick the overridden boss. - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfileOverride); - SetConVarString(g_cvBossProfileOverride, ""); - } - else if (g_bNewBossRound) - { - if (g_bNewBossRoundNew) - { - new Handle:hBossList = GetNewBossRoundProfileList(); - - GetArrayString(hBossList, GetRandomInt(0, GetArraySize(hBossList) - 1), g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile)); - - CloseHandle(hBossList); - } - - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), g_strNewBossRoundProfile); - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossMain, sProfile, sizeof(sProfile)); - - if (strlen(sProfile) > 0 && IsProfileValid(sProfile)) - { - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfile); - } - else - { - if (GetArraySize(hSelectableBossList) > 0) - { - // Pick the first boss in our array if the main boss doesn't exist. - GetArrayString(hSelectableBossList, 0, g_strRoundBossProfile, sizeof(g_strRoundBossProfile)); - } - else - { - // No bosses to pick. What? - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), ""); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END SelectStartingBossesForRound() -> boss: %s", g_strRoundBossProfile); -#endif -} - -static GetRoundIntroParameters() -{ - g_iRoundIntroFadeColor[0] = 0; - g_iRoundIntroFadeColor[1] = 0; - g_iRoundIntroFadeColor[2] = 0; - g_iRoundIntroFadeColor[3] = 255; - - g_flRoundIntroFadeHoldTime = GetConVarFloat(g_cvIntroDefaultHoldTime); - g_flRoundIntroFadeDuration = GetConVarFloat(g_cvIntroDefaultFadeTime); - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "env_fade")) != -1) - { - decl String:sName[32]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_intro_fade", false)) - { - new iColorOffset = FindSendPropOffs("CBaseEntity", "m_clrRender"); - if (iColorOffset != -1) - { - g_iRoundIntroFadeColor[0] = GetEntData(ent, iColorOffset, 1); - g_iRoundIntroFadeColor[1] = GetEntData(ent, iColorOffset + 1, 1); - g_iRoundIntroFadeColor[2] = GetEntData(ent, iColorOffset + 2, 1); - g_iRoundIntroFadeColor[3] = GetEntData(ent, iColorOffset + 3, 1); - } - - g_flRoundIntroFadeHoldTime = GetEntPropFloat(ent, Prop_Data, "m_HoldTime"); - g_flRoundIntroFadeDuration = GetEntPropFloat(ent, Prop_Data, "m_Duration"); - - break; - } - } - - // Get the intro music. - strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), SF2_INTRO_DEFAULT_MUSIC); - - ent = -1; - while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) - { - decl String:sName[64]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrEqual(sName, "sf2_intro_music", false)) - { - decl String:sSongPath[PLATFORM_MAX_PATH]; - GetEntPropString(ent, Prop_Data, "m_iszSound", sSongPath, sizeof(sSongPath)); - - if (strlen(sSongPath) == 0) - { - LogError("Found sf2_intro_music entity, but it has no sound path specified! Default intro music will be used instead."); - } - else - { - strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), sSongPath); - } - - break; - } - } -} - -static GetRoundEscapeParameters() -{ - g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; - - decl String:sName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (!StrContains(sName, "sf2_escape_spawnpoint", false)) - { - g_iRoundEscapePointEntity = EntIndexToEntRef(ent); - break; - } - } -} - -InitializeNewGame() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeNewGame()"); -#endif - - GetRoundIntroParameters(); - GetRoundEscapeParameters(); - - // Choose round state. - if (GetConVarBool(g_cvIntroEnabled)) - { - // Set the round state to the intro stage. - SetRoundState(SF2RoundState_Intro); - } - else - { - SetRoundState(SF2RoundState_Active); - } - - if (g_iRoundActiveCount == 1) - { - SetConVarString(g_cvBossProfileOverride, ""); - } - - HandleSpecialRoundState(); - - // Was a new special round initialized? - if (g_bSpecialRound) - { - if (g_bSpecialRoundNew) - { - // Reset round count. - g_iSpecialRoundCount = 1; - - if (g_bSpecialRoundContinuous) - { - // It's the start of a continuous special round. - - // Initialize all players' values. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedSpecialRound[i] = true; - continue; - } - - g_bPlayerPlayedSpecialRound[i] = false; - } - } - - SpecialRoundCycleStart(); - } - else - { - SpecialRoundStart(); - - if (g_bSpecialRoundContinuous) - { - // Display the current special round going on to late players. - CreateTimer(3.0, Timer_DisplaySpecialRound, _, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - else - { - g_iSpecialRoundCount++; - - SpecialRoundReset(); - } - - // Determine boss round state. - HandleNewBossRoundState(); - - if (g_bNewBossRound) - { - if (g_bNewBossRoundNew) - { - // Reset round count; - g_iNewBossRoundCount = 1; - - if (g_bNewBossRoundContinuous) - { - // It's the start of a continuous "new boss round". - - // Initialize all players' values. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedNewBossRound[i] = true; - continue; - } - - g_bPlayerPlayedNewBossRound[i] = false; - } - } - } - } - else - { - g_iNewBossRoundCount++; - } - - InitializeMapEntities(); - - // Initialize pages and entities. - GetPageMusicRanges(); - - SelectStartingBossesForRound(); - - ForceInNextPlayersInQueue(GetMaxPlayersForRound()); - - // Respawn all players, if needed. - for (new i = 1; i <= MaxClients; i++) - { - if(IsValidClient(i)) { - if (IsClientParticipating(i)) - { - if (!HandlePlayerTeam(i)) - { - TF2_RespawnPlayer(i); - } - /* - if(g_bPlayerEliminated[i]) { - ClientSetGhostModeState(i, true); - PrintToServer("IsClientParticipating(%d): %d", i, IsClientParticipating(i)); - PrintToServer("g_bPlayerEliminated[%d]: %d", i, g_bPlayerEliminated[i]); - PrintToServer("IsClientInGhostMode(%d): %d", i, IsClientInGhostMode(i)); - } - */ - } - //PrintToServer("IsClientInGhostMode(i): %d", IsClientInGhostMode(i)); - //if(IsClientInGhostMode(i)) ClientGhostModeNextTarget(i); - } - } - - if (GetRoundState() == SF2RoundState_Intro) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - if (!IsFakeClient(i)) - { - // Currently in intro state, play intro music. - g_hPlayerIntroMusicTimer[i] = CreateTimer(0.5, Timer_PlayIntroMusicToPlayer, GetClientUserId(i), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; - } - } - else - { - g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; - /* - PrintToServer("IsClientInGhostMode(i): %d", IsClientInGhostMode(i)); - if(IsClientInGhostMode(i)) ClientGhostModeNextTarget(i); - */ - } - } - } - else - { - // Spawn the boss! - SelectProfile(0, g_strRoundBossProfile); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeNewGame()"); -#endif -} - -public Action:Timer_PlayIntroMusicToPlayer(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerIntroMusicTimer[client]) return; - - g_hPlayerIntroMusicTimer[client] = INVALID_HANDLE; - - EmitSoundToClient(client, g_strRoundIntroMusic, _, MUSIC_CHAN, SNDLEVEL_NONE); -} - -public Action:Timer_IntroTextSequence(Handle:timer) -{ - if (!g_bEnabled) return; - if (g_hRoundIntroTextTimer != timer) return; - - new Float:flDuration = 0.0; - - if (g_iRoundIntroText != 0) - { - new bool:bFoundGameText = false; - - new iClients[MAXPLAYERS + 1]; - new iClientsNum; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - - iClients[iClientsNum] = i; - iClientsNum++; - } - - if (!g_bRoundIntroTextDefault) - { - decl String:sTargetname[64]; - Format(sTargetname, sizeof(sTargetname), "sf2_intro_text_%d", g_iRoundIntroText); - - new iGameText = FindEntityByTargetname(sTargetname, "game_text"); - if (iGameText && iGameText != INVALID_ENT_REFERENCE) - { - bFoundGameText = true; - flDuration = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); - - decl String:sMessage[512]; - GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); - } - } - else - { - if (g_iRoundIntroText == 2) - { - bFoundGameText = false; - - decl String:sMessage[64]; - GetCurrentMap(sMessage, sizeof(sMessage)); - - for (new i = 0; i < iClientsNum; i++) - { - ClientShowMainMessage(iClients[i], sMessage, 1); - } - } - } - - if (g_iRoundIntroText == 1 && !bFoundGameText) - { - // Use default intro sequence. Eugh. - g_bRoundIntroTextDefault = true; - flDuration = GetConVarFloat(g_cvIntroDefaultHoldTime) / 2.0; - - for (new i = 0; i < iClientsNum; i++) - { - EmitSoundToClient(iClients[i], SF2_INTRO_DEFAULT_MUSIC, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - else - { - if (!bFoundGameText) return; // done with sequence; don't check anymore. - } - } - - g_iRoundIntroText++; - g_hRoundIntroTextTimer = CreateTimer(flDuration, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ActivateRoundFromIntro(Handle:timer) -{ - if (!g_bEnabled) return; - if (g_hRoundIntroTimer != timer) return; - - // Obviously we don't want to spawn the boss when g_strRoundBossProfile isn't set yet. - SetRoundState(SF2RoundState_Active); - - // Spawn the boss! - SelectProfile(0, g_strRoundBossProfile); -} - -CheckRoundWinConditions() -{ - if (IsRoundInWarmup() || IsRoundEnding()) return; - - new iTotalCount = 0; - new iAliveCount = 0; - new iEscapedCount = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - iTotalCount++; - if (!g_bPlayerEliminated[i] && !IsClientInDeathCam(i)) - { - iAliveCount++; - if (DidClientEscape(i)) iEscapedCount++; - } - } - - if (iAliveCount == 0) - { - ForceTeamWin(_:TFTeam_Blue); - } - else - { - if (g_bRoundHasEscapeObjective) - { - if (iEscapedCount == iAliveCount) - { - ForceTeamWin(_:TFTeam_Red); - } - } - else - { - if (g_iPageMax > 0 && g_iPageCount == g_iPageMax) - { - ForceTeamWin(_:TFTeam_Red); - } - } - } -} - -// ========================================================== -// API -// ========================================================== - -public Native_IsRunning(Handle:plugin, numParams) -{ - return g_bEnabled; -} - -public Native_GetCurrentDifficulty(Handle:plugin, numParams) -{ - return GetConVarInt(g_cvDifficulty); -} - -public Native_GetDifficultyModifier(Handle:plugin, numParams) -{ - new iDifficulty = GetNativeCell(1); - if (iDifficulty < Difficulty_Easy || iDifficulty >= Difficulty_Max) - { - LogError("Difficulty parameter can only be from %d to %d!", Difficulty_Easy, Difficulty_Max - 1); - return _:1.0; - } - - switch (iDifficulty) - { - case Difficulty_Easy: return _:DIFFICULTY_EASY; - case Difficulty_Hard: return _:DIFFICULTY_HARD; - case Difficulty_Insane: return _:DIFFICULTY_INSANE; - } - - return _:DIFFICULTY_NORMAL; -} - -public Native_IsClientEliminated(Handle:plugin, numParams) -{ - return g_bPlayerEliminated[GetNativeCell(1)]; -} - -public Native_IsClientInGhostMode(Handle:plugin, numParams) -{ - return IsClientInGhostMode(GetNativeCell(1)); -} - -public Native_IsClientProxy(Handle:plugin, numParams) -{ - return g_bPlayerProxy[GetNativeCell(1)]; -} - -public Native_GetClientBlinkCount(Handle:plugin, numParams) -{ - return ClientGetBlinkCount(GetNativeCell(1)); -} - -public Native_GetClientProxyMaster(Handle:plugin, numParams) -{ - return NPCGetFromUniqueID(g_iPlayerProxyMaster[GetNativeCell(1)]); -} - -public Native_GetClientProxyControlAmount(Handle:plugin, numParams) -{ - return g_iPlayerProxyControl[GetNativeCell(1)]; -} - -public Native_GetClientProxyControlRate(Handle:plugin, numParams) -{ - return _:g_flPlayerProxyControlRate[GetNativeCell(1)]; -} - -public Native_SetClientProxyMaster(Handle:plugin, numParams) -{ - g_iPlayerProxyMaster[GetNativeCell(1)] = NPCGetUniqueID(GetNativeCell(2)); -} - -public Native_SetClientProxyControlAmount(Handle:plugin, numParams) -{ - g_iPlayerProxyControl[GetNativeCell(1)] = GetNativeCell(2); -} - -public Native_SetClientProxyControlRate(Handle:plugin, numParams) -{ - g_flPlayerProxyControlRate[GetNativeCell(1)] = Float:GetNativeCell(2); -} - -public Native_IsClientLookingAtBoss(Handle:plugin, numParams) -{ - return g_bPlayerSeesSlender[GetNativeCell(1)][GetNativeCell(2)]; -} - -public Native_GetMaxBosses(Handle:plugin, numParams) -{ - return MAX_BOSSES; -} - -public Native_EntIndexToBossIndex(Handle:plugin, numParams) -{ - return NPCGetFromEntIndex(GetNativeCell(1)); -} - -public Native_BossIndexToEntIndex(Handle:plugin, numParams) -{ - return NPCGetEntIndex(GetNativeCell(1)); -} - -public Native_BossIDToBossIndex(Handle:plugin, numParams) -{ - return NPCGetFromUniqueID(GetNativeCell(1)); -} - -public Native_BossIndexToBossID(Handle:plugin, numParams) -{ - return NPCGetUniqueID(GetNativeCell(1)); -} - -public Native_GetBossName(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(GetNativeCell(1), sProfile, sizeof(sProfile)); - - SetNativeString(2, sProfile, GetNativeCell(3)); -} - -public Native_GetBossModelEntity(Handle:plugin, numParams) -{ - return EntRefToEntIndex(g_iSlenderModel[GetNativeCell(1)]); -} - -public Native_GetBossTarget(Handle:plugin, numParams) -{ - return EntRefToEntIndex(g_iSlenderTarget[GetNativeCell(1)]); -} - -public Native_GetBossMaster(Handle:plugin, numParams) -{ - return g_iSlenderCopyMaster[GetNativeCell(1)]; -} - -public Native_GetBossState(Handle:plugin, numParams) -{ - return g_iSlenderState[GetNativeCell(1)]; -} - -public Native_IsBossProfileValid(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - return IsProfileValid(sProfile); -} - -public Native_GetBossProfileNum(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - return GetProfileNum(sProfile, sKeyValue, GetNativeCell(3)); -} - -public Native_GetBossProfileFloat(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - return _:GetProfileFloat(sProfile, sKeyValue, Float:GetNativeCell(3)); -} - -public Native_GetBossProfileString(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - new iResultLen = GetNativeCell(4); - decl String:sResult[iResultLen]; - - decl String:sDefaultValue[512]; - GetNativeString(5, sDefaultValue, sizeof(sDefaultValue)); - - new bool:bSuccess = GetProfileString(sProfile, sKeyValue, sResult, iResultLen, sDefaultValue); - - SetNativeString(3, sResult, iResultLen); - return bSuccess; -} - -public Native_GetBossProfileVector(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - decl Float:flResult[3]; - decl Float:flDefaultValue[3]; - GetNativeArray(4, flDefaultValue, 3); - - new bool:bSuccess = GetProfileVector(sProfile, sKeyValue, flResult, flDefaultValue); - - SetNativeArray(3, flResult, 3); - return bSuccess; -} - -public Native_GetRandomStringFromBossProfile(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - new iBufferLen = GetNativeCell(4); - decl String:sBuffer[iBufferLen]; - - new iIndex = GetNativeCell(5); - - new bool:bSuccess = GetRandomStringFromProfile(sProfile, sKeyValue, sBuffer, iBufferLen, iIndex); - SetNativeString(3, sBuffer, iBufferLen); - return bSuccess; +#include <sourcemod> +#include <sdktools> +#include <sdkhooks> +#include <clientprefs> +#include <steamtools> +#include <tf2items> +#include <dhooks> +#include <navmesh> + +#include <tf2> +#include <tf2_stocks> +#include <morecolors> +#include <sf2> + +#undef REQUIRE_PLUGIN +#include <adminmenu> +#tryinclude <store/store-tf2footprints> +#define REQUIRE_PLUGIN + +//#define DEBUG + +// If compiling with SM 1.7+, uncomment to compile and use SF2 methodmaps. +//#define METHODMAPS + +#define PLUGIN_VERSION "0.2.6-git136" +#define PLUGIN_VERSION_DISPLAY "0.2.6" + +public Plugin:myinfo = +{ + name = "RYTP Horror (Slender Fortress edit by lexuzieel special for Penek-Gaming.Ru)", + author = "KitRifty", + description = "Based on the game Slender: The Eight Pages.", + version = PLUGIN_VERSION, + url = "http://steamcommunity.com/groups/SlenderFortress" +} + +#define FILE_RESTRICTEDWEAPONS "configs/sf2/restrictedweapons.cfg" + +#define BOSS_THINKRATE 0.1 // doesn't really matter much since timers go at a minimum of 0.1 seconds anyways + +#define CRIT_SOUND "player/crit_hit.wav" +#define CRIT_PARTICLENAME "crit_text" + +#define PAGE_MODEL "models/rytp/horror/props/hint_paper.mdl" +#define PAGE_MODELSCALE 1.1 + +#define FLASHLIGHT_CLICKSOUND "rytp_horror/toggleflashlight.wav" +#define FLASHLIGHT_BREAKSOUND "ambient/energy/spark6.wav" +#define FLASHLIGHT_NOSOUND "player/suit_denydevice.wav" +#define PAGE_GRABSOUND "rytp_horror/grabpage_sound.wav" + +#define MUSIC_CHAN SNDCHAN_AUTO + +#define MUSIC_GOTPAGES1_SOUND "rytp_horror/grabpage_music_1.wav" +#define MUSIC_GOTPAGES2_SOUND "rytp_horror/grabpage_music_2.wav" +#define MUSIC_GOTPAGES3_SOUND "rytp_horror/grabpage_music_3.wav" +#define MUSIC_GOTPAGES4_SOUND "rytp_horror/grabpage_music_4.wav" +#define MUSIC_PAGE_VOLUME 1.0 + +#define SF2_INTRO_DEFAULT_MUSIC "rytp_horror/intro_music.mp3" + +#define SF2_HUD_TEXT_COLOR_R 127 +#define SF2_HUD_TEXT_COLOR_G 167 +#define SF2_HUD_TEXT_COLOR_B 141 +#define SF2_HUD_TEXT_COLOR_A 255 + +enum MuteMode +{ + MuteMode_Normal = 0, + MuteMode_DontHearOtherTeam, + MuteMode_DontHearOtherTeamIfNotProxy +}; + +// Offsets. +new g_offsPlayerFOV = -1; +new g_offsPlayerDefaultFOV = -1; +new g_offsPlayerFogCtrl = -1; +new g_offsPlayerPunchAngle = -1; +new g_offsPlayerPunchAngleVel = -1; +new g_offsFogCtrlEnable = -1; +new g_offsFogCtrlEnd = -1; + +new g_iParticleCriticalHit = -1; + +new bool:g_bEnabled; + +new Handle:g_hConfig; +new Handle:g_hRestrictedWeaponsConfig; +new Handle:g_hSpecialRoundsConfig; + +new Handle:g_hPageMusicRanges; + +new g_iSlenderModel[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new g_iSlenderPoseEnt[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new g_iSlenderCopyMaster[MAX_BOSSES] = { -1, ... }; +new Float:g_flSlenderEyePosOffset[MAX_BOSSES][3]; +new Float:g_flSlenderEyeAngOffset[MAX_BOSSES][3]; +new Float:g_flSlenderDetectMins[MAX_BOSSES][3]; +new Float:g_flSlenderDetectMaxs[MAX_BOSSES][3]; +new Handle:g_hSlenderThink[MAX_BOSSES]; +new Handle:g_hSlenderEntityThink[MAX_BOSSES]; +new Handle:g_hSlenderFakeTimer[MAX_BOSSES]; +new Float:g_flSlenderLastKill[MAX_BOSSES]; +new g_iSlenderState[MAX_BOSSES]; +new g_iSlenderTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new Float:g_flSlenderAcceleration[MAX_BOSSES]; +new Float:g_flSlenderGoalPos[MAX_BOSSES][3]; +new Float:g_flSlenderStaticRadius[MAX_BOSSES]; +new Float:g_flSlenderChaseDeathPosition[MAX_BOSSES][3]; +new bool:g_bSlenderChaseDeathPosition[MAX_BOSSES]; +new Float:g_flSlenderIdleAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderWalkAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderRunAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderJumpSpeed[MAX_BOSSES]; +new Float:g_flSlenderPathNodeTolerance[MAX_BOSSES]; +new Float:g_flSlenderPathNodeLookAhead[MAX_BOSSES]; +new bool:g_bSlenderFeelerReflexAdjustment[MAX_BOSSES]; +new Float:g_flSlenderFeelerReflexAdjustmentPos[MAX_BOSSES][3]; + +new g_iSlenderTeleportTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; + +new Float:g_flSlenderNextTeleportTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportTargetTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMinRange[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxRange[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxTargetTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxTargetStress[MAX_BOSSES] = { 0.0, ... }; +new Float:g_flSlenderTeleportPlayersRestTime[MAX_BOSSES][MAXPLAYERS + 1]; + +// For boss type 2 +// General variables +new g_iSlenderHealth[MAX_BOSSES]; +new Handle:g_hSlenderPath[MAX_BOSSES]; +new g_iSlenderCurrentPathNode[MAX_BOSSES] = { -1, ... }; +new bool:g_bSlenderAttacking[MAX_BOSSES]; +new Handle:g_hSlenderAttackTimer[MAX_BOSSES]; +new Float:g_flSlenderNextJump[MAX_BOSSES] = { -1.0, ... }; +new g_iSlenderInterruptConditions[MAX_BOSSES]; +new Float:g_flSlenderLastFoundPlayer[MAX_BOSSES][MAXPLAYERS + 1]; +new Float:g_flSlenderLastFoundPlayerPos[MAX_BOSSES][MAXPLAYERS + 1][3]; +new Float:g_flSlenderNextPathTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderCalculatedWalkSpeed[MAX_BOSSES]; +new Float:g_flSlenderCalculatedSpeed[MAX_BOSSES]; +new Float:g_flSlenderTimeUntilNoPersistence[MAX_BOSSES]; + +new Float:g_flSlenderProxyTeleportMinRange[MAX_BOSSES]; +new Float:g_flSlenderProxyTeleportMaxRange[MAX_BOSSES]; + +// Sound variables +new Float:g_flSlenderTargetSoundLastTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTargetSoundMasterPos[MAX_BOSSES][3]; // to determine hearing focus +new Float:g_flSlenderTargetSoundTempPos[MAX_BOSSES][3]; +new Float:g_flSlenderTargetSoundDiscardMasterPosTime[MAX_BOSSES]; +new bool:g_bSlenderInvestigatingSound[MAX_BOSSES]; +new SoundType:g_iSlenderTargetSoundType[MAX_BOSSES] = { SoundType_None, ... }; +new g_iSlenderTargetSoundCount[MAX_BOSSES]; +new Float:g_flSlenderLastHeardVoice[MAX_BOSSES]; +new Float:g_flSlenderLastHeardFootstep[MAX_BOSSES]; +new Float:g_flSlenderLastHeardWeapon[MAX_BOSSES]; + + +new Float:g_flSlenderNextJumpScare[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextVoiceSound[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextMoanSound[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextWanderPos[MAX_BOSSES] = { -1.0, ... }; + + +new Float:g_flSlenderTimeUntilRecover[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilAlert[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilIdle[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilChase[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilKill[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilNextProxy[MAX_BOSSES] = { -1.0, ... }; + +// Page data. +new g_iPageCount; +new g_iPageMax; +new Float:g_flPageFoundLastTime; +new bool:g_bPageRef; +new String:g_strPageRefModel[PLATFORM_MAX_PATH]; +new Float:g_flPageRefModelScale; + +static Handle:g_hPlayerIntroMusicTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Seeing Mr. Slendy data. +new bool:g_bPlayerSeesSlender[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerSeesSlenderLastTime[MAXPLAYERS + 1][MAX_BOSSES]; + +new Float:g_flPlayerSightSoundNextTime[MAXPLAYERS + 1][MAX_BOSSES]; + +new Float:g_flPlayerScareLastTime[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerScareNextTime[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerStaticAmount[MAXPLAYERS + 1]; + +new Float:g_flPlayerLastChaseBossEncounterTime[MAXPLAYERS + 1][MAX_BOSSES]; + +// Player static data. +new g_iPlayerStaticMode[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerStaticIncreaseRate[MAXPLAYERS + 1]; +new Float:g_flPlayerStaticDecreaseRate[MAXPLAYERS + 1]; +new Handle:g_hPlayerStaticTimer[MAXPLAYERS + 1]; +new g_iPlayerStaticMaster[MAXPLAYERS + 1] = { -1, ... }; +new String:g_strPlayerStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new String:g_strPlayerLastStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerLastStaticTime[MAXPLAYERS + 1]; +new Float:g_flPlayerLastStaticVolume[MAXPLAYERS + 1]; +new Handle:g_hPlayerLastStaticTimer[MAXPLAYERS + 1]; + +// Static shake data. +new g_iPlayerStaticShakeMaster[MAXPLAYERS + 1]; +new bool:g_bPlayerInStaticShake[MAXPLAYERS + 1]; +new String:g_strPlayerStaticShakeSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerStaticShakeMinVolume[MAXPLAYERS + 1]; +new Float:g_flPlayerStaticShakeMaxVolume[MAXPLAYERS + 1]; + +// Fake lag compensation for FF. +new bool:g_bPlayerLagCompensation[MAXPLAYERS + 1]; +new g_iPlayerLagCompensationTeam[MAXPLAYERS + 1]; + +// Hint data. +enum +{ + PlayerHint_Sprint = 0, + PlayerHint_Flashlight, + PlayerHint_MainMenu, + PlayerHint_Blink, + PlayerHint_MaxNum +}; + +enum PlayerPreferences +{ + bool:PlayerPreference_PvPAutoSpawn, + MuteMode:PlayerPreference_MuteMode, + bool:PlayerPreference_FilmGrain, + bool:PlayerPreference_ShowHints, + bool:PlayerPreference_EnableProxySelection, + bool:PlayerPreference_ProjectedFlashlight, + bool:PlayerPreference_GhostOverlay +}; + +new bool:g_bPlayerHints[MAXPLAYERS + 1][PlayerHint_MaxNum]; +new g_iPlayerPreferences[MAXPLAYERS + 1][PlayerPreferences]; + +// Player data. +new g_iPlayerLastButtons[MAXPLAYERS + 1]; +new bool:g_bPlayerChoseTeam[MAXPLAYERS + 1]; +new bool:g_bPlayerEliminated[MAXPLAYERS + 1]; +new bool:g_bPlayerEscaped[MAXPLAYERS + 1]; +new g_iPlayerPageCount[MAXPLAYERS + 1]; +new g_iPlayerQueuePoints[MAXPLAYERS + 1]; +new bool:g_bPlayerPlaying[MAXPLAYERS + 1]; +new Handle:g_hPlayerOverlayCheck[MAXPLAYERS + 1]; + +new Handle:g_hPlayerSwitchBlueTimer[MAXPLAYERS + 1]; + +// Player stress data. +new Float:g_flPlayerStress[MAXPLAYERS + 1]; +new Float:g_flPlayerStressNextUpdateTime[MAXPLAYERS + 1]; + +// Proxy data. +new bool:g_bPlayerProxy[MAXPLAYERS + 1]; +new bool:g_bPlayerProxyAvailable[MAXPLAYERS + 1]; +new Handle:g_hPlayerProxyAvailableTimer[MAXPLAYERS + 1]; +new bool:g_bPlayerProxyAvailableInForce[MAXPLAYERS + 1]; +new g_iPlayerProxyAvailableCount[MAXPLAYERS + 1]; +new g_iPlayerProxyMaster[MAXPLAYERS + 1]; +new g_iPlayerProxyControl[MAXPLAYERS + 1]; +new Handle:g_hPlayerProxyControlTimer[MAXPLAYERS + 1]; +new Float:g_flPlayerProxyControlRate[MAXPLAYERS + 1]; +new Handle:g_flPlayerProxyVoiceTimer[MAXPLAYERS + 1]; +new g_iPlayerProxyAskMaster[MAXPLAYERS + 1] = { -1, ... }; +new Float:g_iPlayerProxyAskPosition[MAXPLAYERS + 1][3]; + +new g_iPlayerDesiredFOV[MAXPLAYERS + 1]; + +new Handle:g_hPlayerPostWeaponsTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Music system. +new g_iPlayerMusicFlags[MAXPLAYERS + 1]; +new String:g_strPlayerMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerMusicVolume[MAXPLAYERS + 1]; +new Float:g_flPlayerMusicTargetVolume[MAXPLAYERS + 1]; +new Handle:g_hPlayerMusicTimer[MAXPLAYERS + 1]; +new g_iPlayerPageMusicMaster[MAXPLAYERS + 1]; + +// Chase music system, which apparently also uses the alert song system. And the idle sound system. +new String:g_strPlayerChaseMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new String:g_strPlayerChaseMusicSee[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerChaseMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerChaseMusicSeeVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerChaseMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerChaseMusicSeeTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayerChaseMusicMaster[MAXPLAYERS + 1] = { -1, ... }; +new g_iPlayerChaseMusicSeeMaster[MAXPLAYERS + 1] = { -1, ... }; + +new String:g_strPlayerAlertMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerAlertMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerAlertMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayerAlertMusicMaster[MAXPLAYERS + 1] = { -1, ... }; + +new String:g_strPlayer20DollarsMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayer20DollarsMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayer20DollarsMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayer20DollarsMusicMaster[MAXPLAYERS + 1] = { -1, ... }; + + +new SF2RoundState:g_iRoundState = SF2RoundState_Invalid; +new bool:g_bRoundGrace = false; +new Float:g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; +new bool:g_bRoundInfiniteFlashlight = false; +new bool:g_bRoundInfiniteBlink = false; +new bool:g_bRoundInfiniteSprint = false; + +static Handle:g_hRoundGraceTimer = INVALID_HANDLE; +static Handle:g_hRoundTimer = INVALID_HANDLE; +static Handle:g_hVoteTimer = INVALID_HANDLE; +static String:g_strRoundBossProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + +static g_iRoundCount = 0; +static g_iRoundEndCount = 0; +static g_iRoundActiveCount = 0; +static g_iRoundTime = 0; +static g_iRoundTimeLimit = 0; +static g_iRoundEscapeTimeLimit = 0; +static g_iRoundTimeGainFromPage = 0; +static bool:g_bRoundHasEscapeObjective = false; + +static g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; + +static g_iRoundIntroFadeColor[4] = { 255, ... }; +static Float:g_flRoundIntroFadeHoldTime; +static Float:g_flRoundIntroFadeDuration; +static Handle:g_hRoundIntroTimer = INVALID_HANDLE; +static bool:g_bRoundIntroTextDefault = true; +static Handle:g_hRoundIntroTextTimer = INVALID_HANDLE; +static g_iRoundIntroText; +static String:g_strRoundIntroMusic[PLATFORM_MAX_PATH] = ""; + +static g_iRoundWarmupRoundCount = 0; + +static bool:g_bRoundWaitingForPlayers = false; + +// Special round variables. +new bool:g_bSpecialRound = false; +new g_iSpecialRoundType = 0; +new bool:g_bSpecialRoundNew = false; +new bool:g_bSpecialRoundContinuous = false; +new g_iSpecialRoundCount = 1; +new bool:g_bPlayerPlayedSpecialRound[MAXPLAYERS + 1] = { true, ... }; + +// New boss round variables. +static bool:g_bNewBossRound = false; +static bool:g_bNewBossRoundNew = false; +static bool:g_bNewBossRoundContinuous = false; +static g_iNewBossRoundCount = 1; +static bool:g_bPlayerPlayedNewBossRound[MAXPLAYERS + 1] = { true, ... }; +static String:g_strNewBossRoundProfile[64] = ""; + +static Handle:g_hRoundMessagesTimer = INVALID_HANDLE; +static g_iRoundMessagesNum = 0; + +static Handle:g_hBossCountUpdateTimer = INVALID_HANDLE; +static Handle:g_hClientAverageUpdateTimer = INVALID_HANDLE; + +// Server variables. +new Handle:g_cvVersion; +new Handle:g_cvEnabled; +new Handle:g_cvSlenderMapsOnly; +new Handle:g_cvPlayerViewbobEnabled; +new Handle:g_cvPlayerShakeEnabled; +new Handle:g_cvPlayerShakeFrequencyMax; +new Handle:g_cvPlayerShakeAmplitudeMax; +new Handle:g_cvGraceTime; +new Handle:g_cvAllChat; +new Handle:g_cv20Dollars; +new Handle:g_cvMaxPlayers; +new Handle:g_cvMaxPlayersOverride; +new Handle:g_cvCampingEnabled; +new Handle:g_cvCampingMaxStrikes; +new Handle:g_cvCampingStrikesWarn; +new Handle:g_cvCampingMinDistance; +new Handle:g_cvCampingNoStrikeSanity; +new Handle:g_cvCampingNoStrikeBossDistance; +new Handle:g_cvDifficulty; +new Handle:g_cvBossMain; +new Handle:g_cvBossProfileOverride; +new Handle:g_cvPlayerBlinkRate; +new Handle:g_cvPlayerBlinkHoldTime; +new Handle:g_cvSpecialRoundBehavior; +new Handle:g_cvSpecialRoundForce; +new Handle:g_cvSpecialRoundOverride; +new Handle:g_cvSpecialRoundInterval; +new Handle:g_cvNewBossRoundBehavior; +new Handle:g_cvNewBossRoundInterval; +new Handle:g_cvNewBossRoundForce; +new Handle:g_cvPlayerVoiceDistance; +new Handle:g_cvPlayerVoiceWallScale; +new Handle:g_cvUltravisionEnabled; +new Handle:g_cvUltravisionRadiusRed; +new Handle:g_cvUltravisionRadiusBlue; +new Handle:g_cvUltravisionBrightness; +new Handle:g_cvGhostModeConnectionCheck; +new Handle:g_cvGhostModeConnectionTolerance; +new Handle:g_cvIntroEnabled; +new Handle:g_cvIntroDefaultHoldTime; +new Handle:g_cvIntroDefaultFadeTime; +new Handle:g_cvTimeLimit; +new Handle:g_cvTimeLimitEscape; +new Handle:g_cvTimeGainFromPageGrab; +new Handle:g_cvWarmupRound; +new Handle:g_cvWarmupRoundNum; +new Handle:g_cvPlayerViewbobHurtEnabled; +new Handle:g_cvPlayerViewbobSprintEnabled; +new Handle:g_cvPlayerFakeLagCompensation; +new Handle:g_cvPlayerProxyWaitTime; +new Handle:g_cvPlayerProxyAsk; +new Handle:g_cvHalfZatoichiHealthGain; +new Handle:g_cvBlockSuicideDuringRound; + +new Handle:g_cvPlayerInfiniteSprintOverride; +new Handle:g_cvPlayerInfiniteFlashlightOverride; +new Handle:g_cvPlayerInfiniteBlinkOverride; + +new Handle:g_cvGravity; +new Float:g_flGravity; + +new Handle:g_cvMaxRounds; + +new bool:g_b20Dollars; + +new bool:g_bPlayerShakeEnabled; +new bool:g_bPlayerViewbobEnabled; +new bool:g_bPlayerViewbobHurtEnabled; +new bool:g_bPlayerViewbobSprintEnabled; + +new Handle:g_hHudSync; +new Handle:g_hHudSync2; +new Handle:g_hRoundTimerSync; + +new Handle:g_hCookie; + +// Global forwards. +new Handle:fOnBossAdded; +new Handle:fOnBossSpawn; +new Handle:fOnBossChangeState; +new Handle:fOnBossRemoved; +new Handle:fOnPagesSpawned; +new Handle:fOnClientBlink; +new Handle:fOnClientCaughtByBoss; +new Handle:fOnClientGiveQueuePoints; +new Handle:fOnClientActivateFlashlight; +new Handle:fOnClientDeactivateFlashlight; +new Handle:fOnClientBreakFlashlight; +new Handle:fOnClientEscape; +new Handle:fOnClientLooksAtBoss; +new Handle:fOnClientLooksAwayFromBoss; +new Handle:fOnClientStartDeathCam; +new Handle:fOnClientEndDeathCam; +new Handle:fOnClientGetDefaultWalkSpeed; +new Handle:fOnClientGetDefaultSprintSpeed; +new Handle:fOnClientSpawnedAsProxy; +new Handle:fOnClientDamagedByBoss; +new Handle:fOnGroupGiveQueuePoints; + +new Handle:g_hSDKWeaponScattergun; +new Handle:g_hSDKWeaponPistolScout; +new Handle:g_hSDKWeaponBat; +new Handle:g_hSDKWeaponSniperRifle; +new Handle:g_hSDKWeaponSMG; +new Handle:g_hSDKWeaponKukri; +new Handle:g_hSDKWeaponRocketLauncher; +new Handle:g_hSDKWeaponShotgunSoldier; +new Handle:g_hSDKWeaponShovel; +new Handle:g_hSDKWeaponGrenadeLauncher; +new Handle:g_hSDKWeaponStickyLauncher; +new Handle:g_hSDKWeaponBottle; +new Handle:g_hSDKWeaponMinigun; +new Handle:g_hSDKWeaponShotgunHeavy; +new Handle:g_hSDKWeaponFists; +new Handle:g_hSDKWeaponSyringeGun; +new Handle:g_hSDKWeaponMedigun; +new Handle:g_hSDKWeaponBonesaw; +new Handle:g_hSDKWeaponFlamethrower; +new Handle:g_hSDKWeaponShotgunPyro; +new Handle:g_hSDKWeaponFireaxe; +new Handle:g_hSDKWeaponRevolver; +new Handle:g_hSDKWeaponKnife; +new Handle:g_hSDKWeaponInvis; +new Handle:g_hSDKWeaponShotgunPrimary; +new Handle:g_hSDKWeaponPistol; +new Handle:g_hSDKWeaponWrench; + +new Handle:g_hSDKGetMaxHealth; +new Handle:g_hSDKWantsLagCompensationOnEntity; +new Handle:g_hSDKShouldTransmit; + +#include "rytp_horror/stocks.sp" +#include "rytp_horror/overlay.sp" +#include "rytp_horror/logging.sp" +#include "rytp_horror/debug.sp" +#include "rytp_horror/profiles.sp" +#include "rytp_horror/nav.sp" +#include "rytp_horror/effects.sp" +#include "rytp_horror/playergroups.sp" +#include "rytp_horror/menus.sp" +#include "rytp_horror/pvp.sp" +#include "rytp_horror/client.sp" +#include "rytp_horror/npc.sp" +#include "rytp_horror/specialround.sp" +#include "rytp_horror/adminmenu.sp" + + +#define SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND "ui/item_acquired.wav" + +// ========================================================== +// GENERAL PLUGIN HOOK FUNCTIONS +// ========================================================== + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + RegPluginLibrary("sf2"); + + fOnBossAdded = CreateGlobalForward("SF2_OnBossAdded", ET_Ignore, Param_Cell); + fOnBossSpawn = CreateGlobalForward("SF2_OnBossSpawn", ET_Ignore, Param_Cell); + fOnBossChangeState = CreateGlobalForward("SF2_OnBossChangeState", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + fOnBossRemoved = CreateGlobalForward("SF2_OnBossRemoved", ET_Ignore, Param_Cell); + fOnPagesSpawned = CreateGlobalForward("SF2_OnPagesSpawned", ET_Ignore); + fOnClientBlink = CreateGlobalForward("SF2_OnClientBlink", ET_Ignore, Param_Cell); + fOnClientCaughtByBoss = CreateGlobalForward("SF2_OnClientCaughtByBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientGiveQueuePoints = CreateGlobalForward("SF2_OnClientGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientActivateFlashlight = CreateGlobalForward("SF2_OnClientActivateFlashlight", ET_Ignore, Param_Cell); + fOnClientDeactivateFlashlight = CreateGlobalForward("SF2_OnClientDeactivateFlashlight", ET_Ignore, Param_Cell); + fOnClientBreakFlashlight = CreateGlobalForward("SF2_OnClientBreakFlashlight", ET_Ignore, Param_Cell); + fOnClientEscape = CreateGlobalForward("SF2_OnClientEscape", ET_Ignore, Param_Cell); + fOnClientLooksAtBoss = CreateGlobalForward("SF2_OnClientLooksAtBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientLooksAwayFromBoss = CreateGlobalForward("SF2_OnClientLooksAwayFromBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientStartDeathCam = CreateGlobalForward("SF2_OnClientStartDeathCam", ET_Ignore, Param_Cell, Param_Cell); + fOnClientEndDeathCam = CreateGlobalForward("SF2_OnClientEndDeathCam", ET_Ignore, Param_Cell, Param_Cell); + fOnClientGetDefaultWalkSpeed = CreateGlobalForward("SF2_OnClientGetDefaultWalkSpeed", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientGetDefaultSprintSpeed = CreateGlobalForward("SF2_OnClientGetDefaultSprintSpeed", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientSpawnedAsProxy = CreateGlobalForward("SF2_OnClientSpawnedAsProxy", ET_Ignore, Param_Cell); + fOnClientDamagedByBoss = CreateGlobalForward("SF2_OnClientDamagedByBoss", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell); + fOnGroupGiveQueuePoints = CreateGlobalForward("SF2_OnGroupGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); + + CreateNative("SF2_IsRunning", Native_IsRunning); + CreateNative("SF2_GetCurrentDifficulty", Native_GetCurrentDifficulty); + CreateNative("SF2_GetDifficultyModifier", Native_GetDifficultyModifier); + CreateNative("SF2_IsClientEliminated", Native_IsClientEliminated); + CreateNative("SF2_IsClientInGhostMode", Native_IsClientInGhostMode); + CreateNative("SF2_IsClientProxy", Native_IsClientProxy); + CreateNative("SF2_GetClientBlinkCount", Native_GetClientBlinkCount); + CreateNative("SF2_GetClientProxyMaster", Native_GetClientProxyMaster); + CreateNative("SF2_GetClientProxyControlAmount", Native_GetClientProxyControlAmount); + CreateNative("SF2_GetClientProxyControlRate", Native_GetClientProxyControlRate); + CreateNative("SF2_SetClientProxyMaster", Native_SetClientProxyMaster); + CreateNative("SF2_SetClientProxyControlAmount", Native_SetClientProxyControlAmount); + CreateNative("SF2_SetClientProxyControlRate", Native_SetClientProxyControlRate); + CreateNative("SF2_IsClientLookingAtBoss", Native_IsClientLookingAtBoss); + CreateNative("SF2_CollectAsPage", Native_CollectAsPage); + CreateNative("SF2_GetMaxBossCount", Native_GetMaxBosses); + CreateNative("SF2_EntIndexToBossIndex", Native_EntIndexToBossIndex); + CreateNative("SF2_BossIndexToEntIndex", Native_BossIndexToEntIndex); + CreateNative("SF2_BossIDToBossIndex", Native_BossIDToBossIndex); + CreateNative("SF2_BossIndexToBossID", Native_BossIndexToBossID); + CreateNative("SF2_GetBossName", Native_GetBossName); + CreateNative("SF2_GetBossModelEntity", Native_GetBossModelEntity); + CreateNative("SF2_GetBossTarget", Native_GetBossTarget); + CreateNative("SF2_GetBossMaster", Native_GetBossMaster); + CreateNative("SF2_GetBossState", Native_GetBossState); + CreateNative("SF2_IsBossProfileValid", Native_IsBossProfileValid); + CreateNative("SF2_GetBossProfileNum", Native_GetBossProfileNum); + CreateNative("SF2_GetBossProfileFloat", Native_GetBossProfileFloat); + CreateNative("SF2_GetBossProfileString", Native_GetBossProfileString); + CreateNative("SF2_GetBossProfileVector", Native_GetBossProfileVector); + CreateNative("SF2_GetRandomStringFromBossProfile", Native_GetRandomStringFromBossProfile); + + PvP_InitializeAPI(); + + SpecialRoundInitializeAPI(); + + return APLRes_Success; +} + +public OnPluginStart() +{ + LoadTranslations("core.phrases"); + LoadTranslations("common.phrases"); + LoadTranslations("sf2.phrases"); + + // Get offsets. + g_offsPlayerFOV = FindSendPropInfo("CBasePlayer", "m_iFOV"); + if (g_offsPlayerFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iFOV."); + + g_offsPlayerDefaultFOV = FindSendPropInfo("CBasePlayer", "m_iDefaultFOV"); + if (g_offsPlayerDefaultFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iDefaultFOV."); + + g_offsPlayerFogCtrl = FindSendPropInfo("CBasePlayer", "m_PlayerFog.m_hCtrl"); + if (g_offsPlayerFogCtrl == -1) LogError("Couldn't find CBasePlayer offset for m_PlayerFog.m_hCtrl!"); + + g_offsPlayerPunchAngle = FindSendPropInfo("CBasePlayer", "m_vecPunchAngle"); + if (g_offsPlayerPunchAngle == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngle!"); + + g_offsPlayerPunchAngleVel = FindSendPropInfo("CBasePlayer", "m_vecPunchAngleVel"); + if (g_offsPlayerPunchAngleVel == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngleVel!"); + + g_offsFogCtrlEnable = FindSendPropInfo("CFogController", "m_fog.enable"); + if (g_offsFogCtrlEnable == -1) LogError("Couldn't find CFogController offset for m_fog.enable!"); + + g_offsFogCtrlEnd = FindSendPropInfo("CFogController", "m_fog.end"); + if (g_offsFogCtrlEnd == -1) LogError("Couldn't find CFogController offset for m_fog.end!"); + + g_hPageMusicRanges = CreateArray(3); + + // Register console variables. + g_cvVersion = CreateConVar("sf2_version", PLUGIN_VERSION, "The current version of Slender Fortress. DO NOT TOUCH!", FCVAR_SPONLY | FCVAR_NOTIFY | FCVAR_DONTRECORD); + SetConVarString(g_cvVersion, PLUGIN_VERSION); + + g_cvEnabled = CreateConVar("sf2_enabled", "1", "Enable/Disable the Slender Fortress gamemode. This will take effect on map change.", FCVAR_NOTIFY | FCVAR_DONTRECORD); + g_cvSlenderMapsOnly = CreateConVar("sf2_slendermapsonly", "1", "Only enable the Slender Fortress gamemode on map names prefixed with \"slender_\" or \"sf2_\"."); + + g_cvGraceTime = CreateConVar("sf2_gracetime", "30.0"); + g_cvIntroEnabled = CreateConVar("sf2_intro_enabled", "1"); + g_cvIntroDefaultHoldTime = CreateConVar("sf2_intro_default_hold_time", "9.0"); + g_cvIntroDefaultFadeTime = CreateConVar("sf2_intro_default_fade_time", "1.0"); + + g_cvBlockSuicideDuringRound = CreateConVar("sf2_block_suicide_during_round", "0"); + + g_cvAllChat = CreateConVar("sf2_alltalk", "0"); + HookConVarChange(g_cvAllChat, OnConVarChanged); + + g_cvPlayerVoiceDistance = CreateConVar("sf2_player_voice_distance", "800.0", "The maximum distance RED can communicate in voice chat. Set to 0 if you want them to be heard at all times.", _, true, 0.0); + g_cvPlayerVoiceWallScale = CreateConVar("sf2_player_voice_scale_blocked", "0.5", "The distance required to hear RED in voice chat will be multiplied by this amount if something is blocking them."); + + g_cvPlayerViewbobEnabled = CreateConVar("sf2_player_viewbob_enabled", "1", "Enable/Disable player viewbobbing.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobEnabled, OnConVarChanged); + g_cvPlayerViewbobHurtEnabled = CreateConVar("sf2_player_viewbob_hurt_enabled", "0", "Enable/Disable player view tilting when hurt.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobHurtEnabled, OnConVarChanged); + g_cvPlayerViewbobSprintEnabled = CreateConVar("sf2_player_viewbob_sprint_enabled", "0", "Enable/Disable player step viewbobbing when sprinting.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobSprintEnabled, OnConVarChanged); + g_cvGravity = FindConVar("sv_gravity"); + HookConVarChange(g_cvGravity, OnConVarChanged); + + g_cvPlayerFakeLagCompensation = CreateConVar("sf2_player_fakelagcompensation", "0", "(EXPERIMENTAL) Enable/Disable fake lag compensation for some hitscan weapons such as the Sniper Rifle.", _, true, 0.0, true, 1.0); + + g_cvPlayerShakeEnabled = CreateConVar("sf2_player_shake_enabled", "1", "Enable/Disable player view shake during boss encounters.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerShakeEnabled, OnConVarChanged); + g_cvPlayerShakeFrequencyMax = CreateConVar("sf2_player_shake_frequency_max", "255", "Maximum frequency value of the shake. Should be a value between 1-255.", _, true, 1.0, true, 255.0); + g_cvPlayerShakeAmplitudeMax = CreateConVar("sf2_player_shake_amplitude_max", "5", "Maximum amplitude value of the shake. Should be a value between 1-16.", _, true, 1.0, true, 16.0); + + g_cvPlayerBlinkRate = CreateConVar("sf2_player_blink_rate", "0.33", "How long (in seconds) each bar on the player's Blink meter lasts.", _, true, 0.0); + g_cvPlayerBlinkHoldTime = CreateConVar("sf2_player_blink_holdtime", "0.15", "How long (in seconds) a player will stay in Blink mode when he or she blinks.", _, true, 0.0); + + g_cvUltravisionEnabled = CreateConVar("sf2_player_ultravision_enabled", "1", "Enable/Disable player Ultravision. This helps players see in the dark when their Flashlight is off or unavailable.", _, true, 0.0, true, 1.0); + g_cvUltravisionRadiusRed = CreateConVar("sf2_player_ultravision_radius_red", "512.0"); + g_cvUltravisionRadiusBlue = CreateConVar("sf2_player_ultravision_radius_blue", "800.0"); + g_cvUltravisionBrightness = CreateConVar("sf2_player_ultravision_brightness", "-4"); + + g_cvGhostModeConnectionCheck = CreateConVar("sf2_ghostmode_check_connection", "1", "Checks a player's connection while in Ghost Mode. If the check fails, the client is booted out of Ghost Mode and the action and client's SteamID is logged in the main SF2 log."); + g_cvGhostModeConnectionTolerance = CreateConVar("sf2_ghostmode_connection_tolerance", "5.0", "If sf2_ghostmode_check_connection is set to 1 and the client has timed out for at least this amount of time, the client will be booted out of Ghost Mode."); + + g_cv20Dollars = CreateConVar("sf2_20dollarmode", "0", "Enable/Disable $20 mode.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cv20Dollars, OnConVarChanged); + + g_cvMaxPlayers = CreateConVar("sf2_maxplayers", "5", "The maximum amount of players that can be in one round.", _, true, 1.0); + HookConVarChange(g_cvMaxPlayers, OnConVarChanged); + + g_cvMaxPlayersOverride = CreateConVar("sf2_maxplayers_override", "-1", "Overrides the maximum amount of players that can be in one round.", _, true, -1.0); + HookConVarChange(g_cvMaxPlayersOverride, OnConVarChanged); + + g_cvCampingEnabled = CreateConVar("sf2_anticamping_enabled", "1", "Enable/Disable anti-camping system for RED.", _, true, 0.0, true, 1.0); + g_cvCampingMaxStrikes = CreateConVar("sf2_anticamping_maxstrikes", "4", "How many 5-second intervals players are allowed to stay in one spot before he/she is forced to suicide.", _, true, 0.0); + g_cvCampingStrikesWarn = CreateConVar("sf2_anticamping_strikeswarn", "2", "The amount of strikes left where the player will be warned of camping."); + g_cvCampingMinDistance = CreateConVar("sf2_anticamping_mindistance", "128.0", "Every 5 seconds the player has to be at least this far away from his last position 5 seconds ago or else he'll get a strike."); + g_cvCampingNoStrikeSanity = CreateConVar("sf2_anticamping_no_strike_sanity", "0.1", "The camping system will NOT give any strikes under any circumstances if the players's Sanity is missing at least this much of his maximum Sanity (max is 1.0)."); + g_cvCampingNoStrikeBossDistance = CreateConVar("sf2_anticamping_no_strike_boss_distance", "512.0", "The camping system will NOT give any strikes under any circumstances if the player is this close to a boss (ignoring LOS)."); + g_cvBossMain = CreateConVar("sf2_boss_main", "slenderman", "The name of the main boss (its profile name, not its display name)"); + g_cvBossProfileOverride = CreateConVar("sf2_boss_profile_override", "", "Overrides which boss will be chosen next. Only applies to the first boss being chosen."); + g_cvDifficulty = CreateConVar("sf2_difficulty", "1", "Difficulty of the game. 1 = Normal, 2 = Hard, 3 = Insane.", _, true, 1.0, true, 3.0); + HookConVarChange(g_cvDifficulty, OnConVarChanged); + + g_cvSpecialRoundBehavior = CreateConVar("sf2_specialround_mode", "0", "0 = Special Round resets on next round, 1 = Special Round keeps going until all players have played (not counting spectators, recently joined players, and those who reset their queue points during the round)", _, true, 0.0, true, 1.0); + g_cvSpecialRoundForce = CreateConVar("sf2_specialround_forceenable", "-1", "Sets whether a Special Round will occur on the next round or not.", _, true, -1.0, true, 1.0); + g_cvSpecialRoundOverride = CreateConVar("sf2_specialround_forcetype", "-1", "Sets the type of Special Round that will be chosen on the next Special Round. Set to -1 to let the game choose.", _, true, -1.0); + g_cvSpecialRoundInterval = CreateConVar("sf2_specialround_interval", "5", "If this many rounds are completed, the next round will be a Special Round.", _, true, 0.0); + + g_cvNewBossRoundBehavior = CreateConVar("sf2_newbossround_mode", "0", "0 = boss selection will return to normal after the boss round, 1 = the new boss will continue being the boss until all players in the server have played against it (not counting spectators, recently joined players, and those who reset their queue points during the round).", _, true, 0.0, true, 1.0); + g_cvNewBossRoundInterval = CreateConVar("sf2_newbossround_interval", "3", "If this many rounds are completed, the next round's boss will be randomly chosen, but will not be the main boss.", _, true, 0.0); + g_cvNewBossRoundForce = CreateConVar("sf2_newbossround_forceenable", "-1", "Sets whether a new boss will be chosen on the next round or not. Set to -1 to let the game choose.", _, true, -1.0, true, 1.0); + + g_cvTimeLimit = CreateConVar("sf2_timelimit_default", "300", "The time limit of the round. Maps can change the time limit.", _, true, 0.0); + g_cvTimeLimitEscape = CreateConVar("sf2_timelimit_escape_default", "90", "The time limit to escape. Maps can change the time limit.", _, true, 0.0); + g_cvTimeGainFromPageGrab = CreateConVar("sf2_time_gain_page_grab", "12", "The time gained from grabbing a page. Maps can change the time gain amount."); + + g_cvWarmupRound = CreateConVar("sf2_warmupround", "1", "Enables/disables Warmup Rounds after the \"Waiting for Players\" phase.", _, true, 0.0, true, 1.0); + g_cvWarmupRoundNum = CreateConVar("sf2_warmupround_num", "1", "Sets the amount of Warmup Rounds that occur after the \"Waiting for Players\" phase.", _, true, 0.0); + + g_cvPlayerProxyWaitTime = CreateConVar("sf2_player_proxy_waittime", "35", "How long (in seconds) after a player was chosen to be a Proxy must the system wait before choosing him again."); + g_cvPlayerProxyAsk = CreateConVar("sf2_player_proxy_ask", "0", "Set to 1 if the player can choose before becoming a Proxy, set to 0 to force."); + + g_cvHalfZatoichiHealthGain = CreateConVar("sf2_halfzatoichi_healthgain", "20", "How much health should be gained from killing a player with the Half-Zatoichi? Set to -1 for default behavior."); + + g_cvPlayerInfiniteSprintOverride = CreateConVar("sf2_player_infinite_sprint_override", "-1", "1 = infinite sprint, 0 = never have infinite sprint, -1 = let the game choose.", _, true, -1.0, true, 1.0); + g_cvPlayerInfiniteFlashlightOverride = CreateConVar("sf2_player_infinite_flashlight_override", "-1", "1 = infinite flashlight, 0 = never have infinite flashlight, -1 = let the game choose.", _, true, -1.0, true, 1.0); + g_cvPlayerInfiniteBlinkOverride = CreateConVar("sf2_player_infinite_blink_override", "-1", "1 = infinite blink, 0 = never have infinite blink, -1 = let the game choose.", _, true, -1.0, true, 1.0); + + g_cvMaxRounds = FindConVar("mp_maxrounds"); + + g_hHudSync = CreateHudSynchronizer(); + g_hHudSync2 = CreateHudSynchronizer(); + g_hRoundTimerSync = CreateHudSynchronizer(); + g_hCookie = RegClientCookie("slender_cookie", "", CookieAccess_Private); + + // Register console commands. + RegConsoleCmd("sm_sf2", Command_MainMenu); + RegConsoleCmd("sm_slender", Command_MainMenu); + RegConsoleCmd("sm_horror", Command_MainMenu); + RegConsoleCmd("sm_slnext", Command_Next); + RegConsoleCmd("sm_slgroup", Command_Group); + RegConsoleCmd("sm_slgroupname", Command_GroupName); + RegConsoleCmd("sm_slghost", Command_GhostMode); + RegConsoleCmd("sm_slhelp", Command_Help); + RegConsoleCmd("sm_slsettings", Command_Settings); + RegConsoleCmd("sm_slcredits", Command_Credits); + RegConsoleCmd("sm_flashlight", Command_ToggleFlashlight); + RegConsoleCmd("+sprint", Command_SprintOn); + RegConsoleCmd("-sprint", Command_SprintOff); + + RegAdminCmd("sm_sf2_scare", Command_ClientPerformScare, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_spawn_boss", Command_SpawnSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_add_boss", Command_AddSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_add_boss_fake", Command_AddSlenderFake, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_remove_boss", Command_RemoveSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_getbossindexes", Command_GetBossIndexes, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_setplaystate", Command_ForceState, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_boss_attack_waiters", Command_SlenderAttackWaiters, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_boss_no_teleport", Command_SlenderNoTeleport, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_force_proxy", Command_ForceProxy, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_force_escape", Command_ForceEscape, ADMFLAG_CHEATS); + + // Hook onto existing console commands. + AddCommandListener(Hook_CommandBuild, "build"); + AddCommandListener(Hook_CommandSuicideAttempt, "kill"); + AddCommandListener(Hook_CommandSuicideAttempt, "explode"); + AddCommandListener(Hook_CommandSuicideAttempt, "joinclass"); + AddCommandListener(Hook_CommandSuicideAttempt, "join_class"); + AddCommandListener(Hook_CommandSuicideAttempt, "jointeam"); + AddCommandListener(Hook_CommandSuicideAttempt, "spectate"); + AddCommandListener(Hook_CommandVoiceMenu, "voicemenu"); + AddCommandListener(Hook_CommandSay, "say"); + + // Hook events. + HookEvent("teamplay_round_start", Event_RoundStart); + HookEvent("teamplay_round_win", Event_RoundEnd); + HookEvent("player_team", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("player_team", Event_PlayerTeam); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("post_inventory_application", Event_PostInventoryApplication); + HookEvent("item_found", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("teamplay_teambalanced_player", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("fish_notice", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("fish_notice__arm", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + + // Hook entities. + HookEntityOutput("trigger_multiple", "OnStartTouch", Hook_TriggerOnStartTouch); + HookEntityOutput("trigger_multiple", "OnEndTouch", Hook_TriggerOnEndTouch); + + // Hook usermessages. + HookUserMessage(GetUserMessageId("VoiceSubtitle"), Hook_BlockUserMessage, true); + + // Hook sounds. + AddNormalSoundHook(Hook_NormalSound); + + AddTempEntHook("Fire Bullets", Hook_TEFireBullets); + + InitializeBossProfiles(); + + NPCInitialize(); + + SetupMenus(); + + SetupAdminMenu(); + + SetupClassDefaultWeapons(); + + SetupPlayerGroups(); + + PvP_Initialize(); + + // @TODO: When cvars are finalized, set this to true. + AutoExecConfig(false); + +#if defined DEBUG + InitializeDebug(); +#endif +} + +public OnAllPluginsLoaded() +{ + SetupHooks(); +} + +public OnPluginEnd() +{ + StopPlugin(); +} + +static SetupHooks() +{ + // Check SDKHooks gamedata. + new Handle:hConfig = LoadGameConfigFile("sdkhooks.games"); + if (hConfig == INVALID_HANDLE) SetFailState("Couldn't find SDKHooks gamedata!"); + + StartPrepSDKCall(SDKCall_Entity); + PrepSDKCall_SetFromConf(hConfig, SDKConf_Virtual, "GetMaxHealth"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + if ((g_hSDKGetMaxHealth = EndPrepSDKCall()) == INVALID_HANDLE) + { + SetFailState("Failed to retrieve GetMaxHealth offset from SDKHooks gamedata!"); + } + + CloseHandle(hConfig); + + // Check our own gamedata. + hConfig = LoadGameConfigFile("sf2"); + if (hConfig == INVALID_HANDLE) SetFailState("Could not find SF2 gamedata!"); + + new iOffset = GameConfGetOffset(hConfig, "CTFPlayer::WantsLagCompensationOnEntity"); + g_hSDKWantsLagCompensationOnEntity = DHookCreate(iOffset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, Hook_ClientWantsLagCompensationOnEntity); + if (g_hSDKWantsLagCompensationOnEntity == INVALID_HANDLE) + { + SetFailState("Failed to create hook CTFPlayer::WantsLagCompensationOnEntity offset from SF2 gamedata!"); + } + + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_CBaseEntity); + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_ObjectPtr); + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_Unknown); + + iOffset = GameConfGetOffset(hConfig, "CBaseEntity::ShouldTransmit"); + g_hSDKShouldTransmit = DHookCreate(iOffset, HookType_Entity, ReturnType_Int, ThisPointer_CBaseEntity, Hook_EntityShouldTransmit); + if (g_hSDKShouldTransmit == INVALID_HANDLE) + { + SetFailState("Failed to create hook CBaseEntity::ShouldTransmit offset from SF2 gamedata!"); + } + + DHookAddParam(g_hSDKShouldTransmit, HookParamType_ObjectPtr); + + CloseHandle(hConfig); +} + +static SetupClassDefaultWeapons() +{ + // Scout + g_hSDKWeaponScattergun = PrepareItemHandle("tf_weapon_scattergun", 13, 0, 0, ""); + g_hSDKWeaponPistolScout = PrepareItemHandle("tf_weapon_pistol", 23, 0, 0, ""); + g_hSDKWeaponBat = PrepareItemHandle("tf_weapon_bat", 0, 0, 0, ""); + + // Sniper + g_hSDKWeaponSniperRifle = PrepareItemHandle("tf_weapon_sniperrifle", 14, 0, 0, ""); + g_hSDKWeaponSMG = PrepareItemHandle("tf_weapon_smg", 16, 0, 0, ""); + g_hSDKWeaponKukri = PrepareItemHandle("tf_weapon_club", 3, 0, 0, ""); + + // Soldier + g_hSDKWeaponRocketLauncher = PrepareItemHandle("tf_weapon_rocketlauncher", 18, 0, 0, ""); + g_hSDKWeaponShotgunSoldier = PrepareItemHandle("tf_weapon_shotgun", 10, 0, 0, ""); + g_hSDKWeaponShovel = PrepareItemHandle("tf_weapon_shovel", 6, 0, 0, ""); + + // Demoman + g_hSDKWeaponGrenadeLauncher = PrepareItemHandle("tf_weapon_grenadelauncher", 19, 0, 0, ""); + g_hSDKWeaponStickyLauncher = PrepareItemHandle("tf_weapon_pipebomblauncher", 20, 0, 0, ""); + g_hSDKWeaponBottle = PrepareItemHandle("tf_weapon_bottle", 1, 0, 0, ""); + + // Heavy + g_hSDKWeaponMinigun = PrepareItemHandle("tf_weapon_minigun", 15, 0, 0, ""); + g_hSDKWeaponShotgunHeavy = PrepareItemHandle("tf_weapon_shotgun", 11, 0, 0, ""); + g_hSDKWeaponFists = PrepareItemHandle("tf_weapon_fists", 5, 0, 0, ""); + + // Medic + g_hSDKWeaponSyringeGun = PrepareItemHandle("tf_weapon_syringegun_medic", 17, 0, 0, ""); + g_hSDKWeaponMedigun = PrepareItemHandle("tf_weapon_medigun", 29, 0, 0, ""); + g_hSDKWeaponBonesaw = PrepareItemHandle("tf_weapon_bonesaw", 8, 0, 0, ""); + + // Pyro + g_hSDKWeaponFlamethrower = PrepareItemHandle("tf_weapon_flamethrower", 21, 0, 0, "254 ; 4.0"); + g_hSDKWeaponShotgunPyro = PrepareItemHandle("tf_weapon_shotgun", 12, 0, 0, ""); + g_hSDKWeaponFireaxe = PrepareItemHandle("tf_weapon_fireaxe", 2, 0, 0, ""); + + // Spy + g_hSDKWeaponRevolver = PrepareItemHandle("tf_weapon_revolver", 24, 0, 0, ""); + g_hSDKWeaponKnife = PrepareItemHandle("tf_weapon_knife", 4, 0, 0, ""); + g_hSDKWeaponInvis = PrepareItemHandle("tf_weapon_invis", 297, 0, 0, ""); + + // Engineer + g_hSDKWeaponShotgunPrimary = PrepareItemHandle("tf_weapon_shotgun", 9, 0, 0, ""); + g_hSDKWeaponPistol = PrepareItemHandle("tf_weapon_pistol", 22, 0, 0, ""); + g_hSDKWeaponWrench = PrepareItemHandle("tf_weapon_wrench", 7, 0, 0, ""); +} + +public OnMapStart() +{ + PvP_OnMapStart(); +} + +public OnConfigsExecuted() +{ + if (!GetConVarBool(g_cvEnabled)) + { + StopPlugin(); + } + else + { + if (GetConVarBool(g_cvSlenderMapsOnly)) + { + decl String:sMap[256]; + GetCurrentMap(sMap, sizeof(sMap)); + + if (!StrContains(sMap, "slender_", false) || !StrContains(sMap, "sf2_", false)) + { + StartPlugin(); + } + else + { + LogMessage("%s is not a Slender Fortress map. Plugin disabled!", sMap); + StopPlugin(); + } + } + else + { + StartPlugin(); + } + } +} + +static StartPlugin() +{ + if (g_bEnabled) return; + + g_bEnabled = true; + + InitializeLogging(); + +#if defined DEBUG + InitializeDebugLogging(); +#endif + + // Handle ConVars. + new Handle:hCvar = FindConVar("mp_friendlyfire"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mp_flashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mat_supportflashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mp_autoteambalance"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + g_flGravity = GetConVarFloat(g_cvGravity); + + g_b20Dollars = GetConVarBool(g_cv20Dollars); + + g_bPlayerShakeEnabled = GetConVarBool(g_cvPlayerShakeEnabled); + g_bPlayerViewbobEnabled = GetConVarBool(g_cvPlayerViewbobEnabled); + g_bPlayerViewbobHurtEnabled = GetConVarBool(g_cvPlayerViewbobHurtEnabled); + g_bPlayerViewbobSprintEnabled = GetConVarBool(g_cvPlayerViewbobSprintEnabled); + + decl String:sBuffer[64]; + Format(sBuffer, sizeof(sBuffer), "RYTP Horror", PLUGIN_VERSION_DISPLAY); + Steam_SetGameDescription(sBuffer); + + PrecacheStuff(); + + // Reset special round. + g_bSpecialRound = false; + g_bSpecialRoundNew = false; + g_bSpecialRoundContinuous = false; + g_iSpecialRoundCount = 1; + g_iSpecialRoundType = 0; + + SpecialRoundReset(); + + // Reset boss rounds. + g_bNewBossRound = false; + g_bNewBossRoundNew = false; + g_bNewBossRoundContinuous = false; + g_iNewBossRoundCount = 1; + strcopy(g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile), ""); + + // Reset global round vars. + g_iRoundCount = 0; + g_iRoundEndCount = 0; + g_iRoundActiveCount = 0; + g_iRoundState = SF2RoundState_Invalid; + g_hRoundMessagesTimer = CreateTimer(200.0, Timer_RoundMessages, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_iRoundMessagesNum = 0; + + g_iRoundWarmupRoundCount = 0; + + g_hClientAverageUpdateTimer = CreateTimer(0.2, Timer_ClientAverageUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_hBossCountUpdateTimer = CreateTimer(2.0, Timer_BossCountUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + SetRoundState(SF2RoundState_Waiting); + + ReloadBossProfiles(); + ReloadRestrictedWeapons(); + ReloadSpecialRounds(); + + NPCOnConfigsExecuted(); + + InitializeBossPackVotes(); + SetupTimeLimitTimerForBossPackVote(); + + // Late load compensation. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + OnClientPutInServer(i); + } +} + +static PrecacheStuff() +{ + // Initialize particles. + g_iParticleCriticalHit = PrecacheParticleSystem(CRIT_PARTICLENAME); + + PrecacheSound2(CRIT_SOUND); + + // simple_bot; + PrecacheModel("models/humans/group01/female_01.mdl", true); + + PrecacheModel(PAGE_MODEL, true); + PrecacheModel(GHOST_MODEL, true); + + PrecacheSound2(FLASHLIGHT_CLICKSOUND); + PrecacheSound2(FLASHLIGHT_BREAKSOUND); + PrecacheSound2(FLASHLIGHT_NOSOUND); + PrecacheSound2(PAGE_GRABSOUND); + + PrecacheSound2(MUSIC_GOTPAGES1_SOUND); + PrecacheSound2(MUSIC_GOTPAGES2_SOUND); + PrecacheSound2(MUSIC_GOTPAGES3_SOUND); + PrecacheSound2(MUSIC_GOTPAGES4_SOUND); + + PrecacheSound2(SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); + + for (new i = 0; i < sizeof(g_strPlayerBreathSounds); i++) + { + PrecacheSound2(g_strPlayerBreathSounds[i]); + } + + // Special round. + PrecacheSound2(SR_MUSIC); + PrecacheSound2(SR_SOUND_SELECT); + PrecacheSound2(SF2_INTRO_DEFAULT_MUSIC); + + PrecacheMaterial2(SF2_OVERLAY_DEFAULT); + PrecacheMaterial2(SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); + PrecacheMaterial2(SF2_OVERLAY_GHOST); + + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.mdl"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx80.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx90.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.phy"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.sw.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.vvd"); + + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vmt"); + + // pvp + PvP_Precache(); +} + +static StopPlugin() +{ + if (!g_bEnabled) return; + + g_bEnabled = false; + + // Reset CVars. + new Handle:hCvar = FindConVar("mp_friendlyfire"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + hCvar = FindConVar("mp_flashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + hCvar = FindConVar("mat_supportflashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + // Cleanup bosses. + NPCRemoveAll(); + + // Cleanup clients. + for (new i = 1; i <= MaxClients; i++) + { + ClientResetFlashlight(i); + ClientDeactivateUltravision(i); + ClientDisableConstantGlow(i); + ClientRemoveInteractiveGlow(i); + } + + BossProfilesOnMapEnd(); +} + +public OnMapEnd() +{ + StopPlugin(); +} + +public OnMapTimeLeftChanged() +{ + if (g_bEnabled) + { + SetupTimeLimitTimerForBossPackVote(); + } +} + +public TF2_OnConditionAdded(client, TFCond:cond) +{ + if (cond == TFCond_Taunting) + { + if (IsClientInGhostMode(client)) + { + // Stop ghosties from taunting. + TF2_RemoveCondition(client, TFCond_Taunting); + } + } +} + +public OnGameFrame() +{ + if (!g_bEnabled) return; + + // Process through boss movement. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new iBoss = NPCGetEntIndex(i); + if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; + + if (NPCGetFlags(i) & SFF_MARKEDASFAKE) continue; + + new iType = NPCGetType(i); + + switch (iType) + { + case SF2BossType_Static: + { + decl Float:myPos[3], Float:hisPos[3]; + SlenderGetAbsOrigin(i, myPos); + AddVectors(myPos, g_flSlenderEyePosOffset[i], myPos); + + new iBestPlayer = -1; + new Float:flBestDistance = 16384.0; + new Float:flTempDistance; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !IsPlayerAlive(iClient) || IsClientInGhostMode(iClient) || IsClientInDeathCam(iClient)) continue; + if (!IsPointVisibleToPlayer(iClient, myPos, false, false)) continue; + + GetClientAbsOrigin(iClient, hisPos); + + flTempDistance = GetVectorDistance(myPos, hisPos); + if (flTempDistance < flBestDistance) + { + iBestPlayer = iClient; + flBestDistance = flTempDistance; + } + } + + if (iBestPlayer > 0) + { + SlenderGetAbsOrigin(i, myPos); + GetClientAbsOrigin(iBestPlayer, hisPos); + + if (!SlenderOnlyLooksIfNotSeen(i) || !IsPointVisibleToAPlayer(myPos, false, SlenderUsesBlink(i))) + { + new Float:flTurnRate = NPCGetTurnRate(i); + + if (flTurnRate > 0.0) + { + decl Float:flMyEyeAng[3], Float:ang[3]; + GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); + AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[i], flMyEyeAng); + SubtractVectors(hisPos, myPos, ang); + GetVectorAngles(ang, ang); + ang[0] = 0.0; + ang[1] += (AngleDiff(ang[1], flMyEyeAng[1]) >= 0.0 ? 1.0 : -1.0) * flTurnRate * GetTickInterval(); + ang[2] = 0.0; + + // Take care of angle offsets. + AddVectors(ang, g_flSlenderEyePosOffset[i], ang); + for (new i2 = 0; i2 < 3; i2++) ang[i2] = AngleNormalize(ang[i2]); + + TeleportEntity(iBoss, NULL_VECTOR, ang, NULL_VECTOR); + } + } + } + } + case SF2BossType_Chaser: + { + SlenderChaseBossProcessMovement(i); + } + } + } + + PvP_OnGameFrame(); +} + +// ========================================================== +// COMMANDS AND COMMAND HOOK FUNCTIONS +// ========================================================== + +public Action:Command_Help(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuHelp, client, 30); + return Plugin_Handled; +} + +public Action:Command_Settings(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuSettings, client, 30); + return Plugin_Handled; +} + +public Action:Command_Credits(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuCredits, client, MENU_TIME_FOREVER); + return Plugin_Handled; +} + +public Action:Command_ToggleFlashlight(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return Plugin_Handled; + + if (!IsRoundInWarmup() && !IsRoundInIntro() && !IsRoundEnding() && !DidClientEscape(client)) + { + if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) + { + ClientHandleFlashlight(client); + } + } + + return Plugin_Handled; +} + +public Action:Command_SprintOn(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) + { + ClientHandleSprint(client, true); + } + + return Plugin_Handled; +} + +public Action:Command_SprintOff(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) + { + ClientHandleSprint(client, false); + } + + return Plugin_Handled; +} + +public Action:Command_MainMenu(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuMain, client, 30); + return Plugin_Handled; +} + +public Action:Command_Next(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayQueuePointsMenu(client); + return Plugin_Handled; +} + +public Action:Command_Group(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayGroupMainMenuToClient(client); + return Plugin_Handled; +} + +public Action:Command_GroupName(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_slgroupname <name>"); + return Plugin_Handled; + } + + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return Plugin_Handled; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return Plugin_Handled; + } + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetCmdArg(1, sGroupName, sizeof(sGroupName)); + if (!sGroupName[0]) + { + CPrintToChat(client, "%T", "SF2 Invalid Group Name", client); + return Plugin_Handled; + } + + decl String:sOldGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sOldGroupName, sizeof(sOldGroupName)); + SetPlayerGroupName(iGroupIndex, sGroupName); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) != iGroupIndex) continue; + CPrintToChat(i, "%T", "SF2 Group Name Set", i, sOldGroupName, sGroupName); + } + + return Plugin_Handled; +} + +public Action:Command_GhostMode(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuGhostMode, client, 15); + return Plugin_Handled; +} + +public Action:Hook_CommandSay(client, const String:command[], argc) +{ + if (!g_bEnabled || GetConVarBool(g_cvAllChat)) return Plugin_Continue; + + if (!IsRoundEnding()) + { + if (g_bPlayerEliminated[client]) + { + decl String:sMessage[256]; + GetCmdArgString(sMessage, sizeof(sMessage)); + FakeClientCommand(client, "say_team %s", sMessage); + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_CommandSuicideAttempt(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) return Plugin_Handled; + + if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; + + if (GetConVarBool(g_cvBlockSuicideDuringRound)) + { + if (!g_bRoundGrace && !g_bPlayerEliminated[client] && !DidClientEscape(client)) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_CommandBlockInGhostMode(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) return Plugin_Handled; + if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Hook_CommandVoiceMenu(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) + { + ClientGhostModeNextTarget(client); + return Plugin_Handled; + } + + if (g_bPlayerProxy[client]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) + { + return Plugin_Handled; + } + } + } + + return Plugin_Continue; +} + +public Action:Command_ClientPerformScare(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_scare <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32], String:arg2[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + GetCmdArg(2, arg2, sizeof(arg2)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + ClientPerformScare(target, StringToInt(arg2)); + } + + return Plugin_Handled; +} + +public Action:Command_SpawnSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args == 0) + { + ReplyToCommand(client, "Usage: sm_sf2_spawn_boss <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl Float:eyePos[3], Float:eyeAng[3], Float:endPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(endPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, endPos); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Spawned Boss", client); + LogAction(client, -1, "%N spawned boss %d! (%s)", client, iBossIndex, sProfile); + + return Plugin_Handled; +} + +public Action:Command_RemoveSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args == 0) + { + ReplyToCommand(client, "Usage: sm_sf2_remove_boss <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + NPCRemove(iBossIndex); + + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Removed Boss", client); + LogAction(client, -1, "%N removed boss %d! (%s)", client, iBossIndex, sProfile); + + return Plugin_Handled; +} + +public Action:Command_GetBossIndexes(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + decl String:sMessage[512]; + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + ClientCommand(client, "echo Active Boss Indexes:"); + ClientCommand(client, "echo ----------------------------"); + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + Format(sMessage, sizeof(sMessage), "%d - %s", i, sProfile); + if (NPCGetFlags(i) & SFF_FAKE) + { + StrCat(sMessage, sizeof(sMessage), " (fake)"); + } + + if (g_iSlenderCopyMaster[i] != -1) + { + decl String:sCat[64]; + Format(sCat, sizeof(sCat), " (copy of %d)", g_iSlenderCopyMaster[i]); + StrCat(sMessage, sizeof(sMessage), sCat); + } + + ClientCommand(client, "echo %s", sMessage); + } + + ClientCommand(client, "echo ----------------------------"); + + ReplyToCommand(client, "Printed active boss indexes to your console!"); + + return Plugin_Handled; +} + +public Action:Command_SlenderAttackWaiters(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_boss_attack_waiters <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossFlags = NPCGetFlags(iBossIndex); + + new bool:bState = bool:StringToInt(arg2); + new bool:bOldState = bool:(iBossFlags & SFF_ATTACKWAITERS); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (bState) + { + if (!bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags | SFF_ATTACKWAITERS); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Attack Waiters", client); + LogAction(client, -1, "%N forced boss %d to attack waiters! (%s)", client, iBossIndex, sProfile); + } + } + else + { + if (bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags & ~SFF_ATTACKWAITERS); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Do Not Attack Waiters", client); + LogAction(client, -1, "%N forced boss %d to not attack waiters! (%s)", client, iBossIndex, sProfile); + } + } + + return Plugin_Handled; +} + +public Action:Command_SlenderNoTeleport(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_boss_no_teleport <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossFlags = NPCGetFlags(iBossIndex); + + new bool:bState = bool:StringToInt(arg2); + new bool:bOldState = bool:(iBossFlags & SFF_NOTELEPORT); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (bState) + { + if (!bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags | SFF_NOTELEPORT); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Not Teleport", client); + LogAction(client, -1, "%N disabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); + } + } + else + { + if (bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags & ~SFF_NOTELEPORT); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Teleport", client); + LogAction(client, -1, "%N enabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); + } + } + + return Plugin_Handled; +} + +public Action:Command_ForceProxy(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_force_proxy <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + if (IsRoundEnding() || IsRoundInWarmup()) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + 0, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossIndex = StringToInt(arg2); + if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) + { + ReplyToCommand(client, "Boss index is out of range!"); + return Plugin_Handled; + } + else if (NPCGetUniqueID(iBossIndex) == -1) + { + ReplyToCommand(client, "Boss index is invalid! Boss index not active!"); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new iTarget = target_list[i]; + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(iTarget, sName, sizeof(sName)); + + if (!g_bPlayerEliminated[iTarget]) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Unable To Perform Action On Player In Round", client, sName); + continue; + } + + if (g_bPlayerProxy[iTarget]) continue; + + decl Float:flNewPos[3]; + + if (!SlenderCalculateNewPlace(iBossIndex, flNewPos, true, true, client)) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Player No Place For Proxy", client, sName); + continue; + } + + ClientEnableProxy(iTarget, iBossIndex); + TeleportEntity(iTarget, flNewPos, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + + LogAction(client, iTarget, "%N forced %N to be a Proxy!", client, iTarget); + } + + return Plugin_Handled; +} + +public Action:Command_ForceEscape(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_force_escape <name|#userid>"); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + if (!g_bPlayerEliminated[i] && !DidClientEscape(i)) + { + ClientEscape(target); + TeleportClientToEscapePoint(target); + + LogAction(client, target, "%N forced %N to escape!", client, target); + } + } + + return Plugin_Handled; +} + +public Action:Command_AddSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_add_boss <name>"); + return Plugin_Handled; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetCmdArg(1, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + if (!KvJumpToKey(g_hConfig, sProfile)) + { + ReplyToCommand(client, "That boss does not exist!"); + return Plugin_Handled; + } + + new iBossIndex = AddProfile(sProfile); + if (iBossIndex != -1) + { + decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(flPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, flPos); + + LogAction(client, -1, "%N added a boss! (%s)", client, sProfile); + } + + return Plugin_Handled; +} + +public Action:Command_AddSlenderFake(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_add_boss_fake <name>"); + return Plugin_Handled; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetCmdArg(1, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + if (!KvJumpToKey(g_hConfig, sProfile)) + { + ReplyToCommand(client, "That boss does not exist!"); + return Plugin_Handled; + } + + new iBossIndex = AddProfile(sProfile, SFF_FAKE); + if (iBossIndex != -1) + { + decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(flPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, flPos); + + LogAction(client, -1, "%N added a fake boss! (%s)", client, sProfile); + } + + return Plugin_Handled; +} + +public Action:Command_ForceState(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_setplaystate <name|#userid> <0/1>"); + return Plugin_Handled; + } + + if (IsRoundEnding() || IsRoundInWarmup()) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + 0, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iState = StringToInt(arg2); + + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + GetClientName(target, sName, sizeof(sName)); + + if (iState && g_bPlayerEliminated[target]) + { + SetClientPlayState(target, true); + + CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced In Game", sName); + LogAction(client, target, "%N forced %N into the game.", client, target); + } + else if (!iState && !g_bPlayerEliminated[target]) + { + SetClientPlayState(target, false); + + CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced Out Of Game", sName); + LogAction(client, target, "%N took %N out of the game.", client, target); + } + } + + return Plugin_Handled; +} + +public Action:Hook_CommandBuild(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (!IsClientInPvP(client)) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Timer_BossCountUpdate(Handle:timer) +{ + if (timer != g_hBossCountUpdateTimer) return Plugin_Stop; + + if (!g_bEnabled) return Plugin_Stop; + + new iBossCount = NPCGetCount(); + new iBossPreferredCount; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1 || + g_iSlenderCopyMaster[i] != -1 || + (NPCGetFlags(i) & SFF_FAKE)) + { + continue; + } + + iBossPreferredCount++; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + IsClientInGhostMode(i) || + IsClientInDeathCam(i) || + DidClientEscape(i)) continue; + + // Check if we're near any bosses. + new iClosest = -1; + new Float:flBestDist = SF2_BOSS_PAGE_CALCULATION; + + for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) + { + if (NPCGetUniqueID(iBoss) == -1) continue; + if (NPCGetEntIndex(iBoss) == INVALID_ENT_REFERENCE) continue; + if (NPCGetFlags(iBoss) & SFF_FAKE) continue; + + new Float:flDist = NPCGetDistanceFromEntity(iBoss, i); + if (flDist < flBestDist) + { + iClosest = iBoss; + flBestDist = flDist; + break; + } + } + + if (iClosest != -1) continue; + + iClosest = -1; + flBestDist = SF2_BOSS_PAGE_CALCULATION; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient) || + !IsPlayerAlive(iClient) || + g_bPlayerEliminated[iClient] || + IsClientInGhostMode(iClient) || + IsClientInDeathCam(iClient) || + DidClientEscape(iClient)) + { + continue; + } + + new bool:bwub = false; + for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) + { + if (NPCGetUniqueID(iBoss) == -1) continue; + if (NPCGetFlags(iBoss) & SFF_FAKE) continue; + + if (g_iSlenderTarget[iBoss] == iClient) + { + bwub = true; + break; + } + } + + if (!bwub) continue; + + new Float:flDist = EntityDistanceFromEntity(i, iClient); + if (flDist < flBestDist) + { + iClosest = iClient; + flBestDist = flDist; + } + } + + if (!IsValidClient(iClosest)) + { + // No one's close to this dude? DUDE! WE NEED ANOTHER BOSS! + iBossPreferredCount++; + } + } + + new iDiff = iBossCount - iBossPreferredCount; + if (iDiff) + { + if (iDiff > 0) + { + new iCount = iDiff; + // We need less bosses. Try and see if we can remove some. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_iSlenderCopyMaster[i] == -1) continue; + if (PeopleCanSeeSlender(i, _, false)) continue; + if (NPCGetFlags(i) & SFF_FAKE) continue; + + if (SlenderCanRemove(i)) + { + NPCRemove(i); + iCount--; + } + + if (iCount <= 0) + { + break; + } + } + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + new iCount = RoundToFloor(FloatAbs(float(iDiff))); + // Add new bosses (copy of the first boss). + for (new i = 0; i < MAX_BOSSES && iCount > 0; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + if (g_iSlenderCopyMaster[i] != -1) continue; + if (!(NPCGetFlags(i) & SFF_COPIES)) continue; + + // Get the number of copies I already have and see if I can have more copies. + new iCopyCount; + for (new i2 = 0; i2 < MAX_BOSSES; i2++) + { + if (NPCGetUniqueID(i2) == -1) continue; + if (g_iSlenderCopyMaster[i2] != i) continue; + + iCopyCount++; + } + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + if (iCopyCount >= GetProfileNum(sProfile, "copy_max", 10)) + { + continue; + } + + new iBossIndex = AddProfile(sProfile, _, i); + if (iBossIndex == -1) + { + LogError("Could not add copy for %d: No free slots!", i); + } + + iCount--; + } + } + } + + // Check if we can add some proxies. + if (!g_bRoundGrace) + { + if (NavMesh_Exists()) + { + new Handle:hProxyCandidates = CreateArray(); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) continue; + + if (g_iSlenderCopyMaster[iBossIndex] != -1) continue; // Copies cannot generate proxies. + + if (GetGameTime() < g_flSlenderTimeUntilNextProxy[iBossIndex]) continue; // Proxy spawning hasn't cooled down yet. + + new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); + if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) continue; // No teleport target. + + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumActiveProxies = 0; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) == iBossIndex) + { + iNumActiveProxies++; + } + } + + if (iNumActiveProxies >= iMaxProxies) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d has too many active proxies!", iBossIndex); +#endif + continue; + } + + new Float:flSpawnChanceMin = GetProfileFloat(sProfile, "proxies_spawn_chance_min"); + new Float:flSpawnChanceMax = GetProfileFloat(sProfile, "proxies_spawn_chance_max"); + new Float:flSpawnChanceThreshold = GetProfileFloat(sProfile, "proxies_spawn_chance_threshold") * NPCGetAnger(iBossIndex); + + new Float:flChance = GetRandomFloat(flSpawnChanceMin, flSpawnChanceMax); + if (flChance > flSpawnChanceThreshold) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's chances weren't in his favor!", iBossIndex); +#endif + continue; + } + + new iAvailableProxies = iMaxProxies - iNumActiveProxies; + + new iSpawnNumMin = GetProfileNum(sProfile, "proxies_spawn_num_min"); + new iSpawnNumMax = GetProfileNum(sProfile, "proxies_spawn_num_max"); + + new iSpawnNum = 0; + + // Get a list of people we can transform into a good Proxy. + ClearArray(hProxyCandidates); + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (g_bPlayerProxy[iClient]) continue; + + if (!g_iPlayerPreferences[iClient][PlayerPreference_EnableProxySelection]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your preferences.", iBossIndex); +#endif + continue; + } + + if (!g_bPlayerProxyAvailable[iClient]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your cooldown.", iBossIndex); +#endif + continue; + } + + if (g_bPlayerProxyAvailableInForce[iClient]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're already being forced into a Proxy.", iBossIndex); +#endif + continue; + } + + if (!IsClientParticipating(iClient)) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're not participating.", iBossIndex); +#endif + continue; + } + + PushArrayCell(hProxyCandidates, iClient); + iSpawnNum++; + } + + if (iSpawnNum >= iSpawnNumMax) + { + iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNumMax); + } + else if (iSpawnNum >= iSpawnNumMin) + { + iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNum); + } + + if (iSpawnNum <= 0) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d had a set spawn number of 0!", iBossIndex); +#endif + continue; + } + + decl Float:flTargetPos[3]; + GetClientAbsOrigin(iTeleportTarget, flTargetPos); + + new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); + if (iTargetAreaIndex == -1) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's teleport target is not on the navmesh!", iBossIndex); +#endif + continue; // target is not on the nav mesh. + } + + // Search outwards until travel distance is at maximum range. + new Handle:hAreaArray = CreateArray(2); + new Handle:hAreas = CreateStack(); + NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderProxyTeleportMaxRange[iBossIndex]); + + new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderProxyTeleportMinRange[iBossIndex], g_flSlenderProxyTeleportMaxRange[iBossIndex]); + + { + new iAreaIndex = -1; + new iPoppedAreas = 0; + + while (!IsStackEmpty(hAreas)) + { + PopStackCell(hAreas, iAreaIndex); + new iCostSoFar = NavMeshArea_GetCostSoFar(iAreaIndex); + + if (float(iCostSoFar) >= flTeleportMinRange) + { + new iIndex = PushArrayCell(hAreaArray, iAreaIndex); + SetArrayCell(hAreaArray, iIndex, float(iCostSoFar), 1); + iPoppedAreas++; + } + } + + CloseHandle(hAreas); + + if (iPoppedAreas == 0) + { + // no areas to use! + CloseHandle(hAreaArray); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any sufficient surrounding areas!", iBossIndex); +#endif + + continue; + } +#if defined DEBUG + else + { + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d found %d surrounding areas", iBossIndex, iPoppedAreas); + } +#endif + } + + new Handle:hAreaArrayClose = CreateArray(); + new Handle:hAreaArrayAverage = CreateArray(); + new Handle:hAreaArrayFar = CreateArray(); + + for (new iRangeSection = 1; iRangeSection <= 3; iRangeSection++) + { + new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection - 1) / 3.0); + new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection) / 3.0); + + for (new i = 0, iSize = GetArraySize(hAreaArray); i < iSize; i++) + { + new iAreaIndex = GetArrayCell(hAreaArray, i); + + decl Float:flAreaCenter[3]; + NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); + + decl Float:flTestPos[3]; + decl Float:flEyeOffset[3]; + flEyeOffset[0] = 0.0; + flEyeOffset[1] = 0.0; + flEyeOffset[2] = HalfHumanHeight * 2.0; + + // Check visibility first. + if (IsPointVisibleToAPlayer(flAreaCenter, false, false)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (1)", iBossIndex, iAreaIndex); +#endif + continue; + } + + AddVectors(flAreaCenter, flEyeOffset, flTestPos); + + if (IsPointVisibleToAPlayer(flTestPos, false, false)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (2)", iBossIndex, iAreaIndex); +#endif + + continue; + } + + new iBoss = NPCGetEntIndex(iBossIndex); + + // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. + { + decl Float:flTraceStartPos[3]; + flTraceStartPos[0] = flAreaCenter[0]; + flTraceStartPos[1] = flAreaCenter[1]; + flTraceStartPos[2] = flAreaCenter[2] + (HalfHumanHeight * 2.0); + + decl Float:flTraceMins[3]; + flTraceMins[0] = -20.0; + flTraceMins[1] = -20.0; + flTraceMins[2] = 0.0; + + decl Float:flTraceMaxs[3]; + flTraceMaxs[0] = 20.0; + flTraceMaxs[1] = 20.0; + flTraceMaxs[2] = 0.0; + + new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flAreaCenter, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + decl Float:flTraceHitPos[3]; + TR_GetEndPosition(flTraceHitPos, hTrace); + flTraceHitPos[2] += 1.0; + CloseHandle(hTrace); + + static Float:flTraceSpaceMin[3] = { -20.0, -20.0, 0.0 }; + static Float:flTraceSpaceMax[3] = { 20.0, 20.0, 72.0 }; + + flTraceSpaceMax[2] = HalfHumanHeight * 2.0; + + if (IsSpaceOccupiedPlayer(flTraceHitPos, + flTraceSpaceMin, + flTraceSpaceMax, + iBoss == INVALID_ENT_REFERENCE ? -1 : iBoss)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected too small area index %d! (2)", iBossIndex, iAreaIndex); +#endif + + continue; + } + } + + new bool:bTooNear = false; + + // Check minimum range. + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || + !IsPlayerAlive(iClient) || + g_bPlayerEliminated[iClient] || + DidClientEscape(iClient) || + g_bPlayerProxy[iClient] || + IsClientInGhostMode(iClient)) + { + continue; + } + + decl Float:flTempPos[3]; + GetClientAbsOrigin(iClient, flTempPos); + + if (GetVectorDistance(flAreaCenter, flTempPos) <= flTeleportMinRange) + { + bTooNear = true; + break; + } + } + + if (bTooNear) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected near area index %d!", iBossIndex, iAreaIndex); +#endif + + continue; // This area is too close to a player. + } + + // Check travel distance. + new Float:flDist = Float:GetArrayCell(hAreaArray, i, 1); + if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) + { + switch (iRangeSection) + { + case 1: PushArrayCell(hAreaArrayClose, iAreaIndex); + case 2: PushArrayCell(hAreaArrayAverage, iAreaIndex); + case 3: PushArrayCell(hAreaArrayFar, iAreaIndex); + } + } + } + } + + CloseHandle(hAreaArray); + + // Set the cooldown time! + new Float:flSpawnCooldownMin = GetProfileFloat(sProfile, "proxies_spawn_cooldown_min"); + new Float:flSpawnCooldownMax = GetProfileFloat(sProfile, "proxies_spawn_cooldown_max"); + + g_flSlenderTimeUntilNextProxy[iBossIndex] = GetGameTime() + GetRandomFloat(flSpawnCooldownMin, flSpawnCooldownMax); + + // Randomize the array. + SortADTArray(hProxyCandidates, Sort_Random, Sort_Integer); + + decl Float:flDestinationPos[3]; + + for (new iNum = 0; iNum < iSpawnNum && iNum < iAvailableProxies; iNum++) + { + new iClient = GetArrayCell(hProxyCandidates, iNum); + new iBestAreaIndex = -1; + + if (GetArraySize(hAreaArrayClose) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayClose, GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1)); + } + else if (GetArraySize(hAreaArrayAverage) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayAverage, GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1)); + } + else if (GetArraySize(hAreaArrayFar) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayFar, GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1)); + } + + if (iBestAreaIndex == -1) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any areas to place proxies (spawned %d)!", iBossIndex, iNum); +#endif + break; + } + + NavMeshArea_GetCenter(iBestAreaIndex, flDestinationPos); + + if (!GetConVarBool(g_cvPlayerProxyAsk)) + { + ClientStartProxyForce(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); + } + else + { + DisplayProxyAskMenu(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); + } + } + + CloseHandle(hAreaArrayClose); + CloseHandle(hAreaArrayAverage); + CloseHandle(hAreaArrayFar); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d finished proxy process!", iBossIndex); +#endif + } + + CloseHandle(hProxyCandidates); + } + } + + return Plugin_Continue; +} + +ReloadRestrictedWeapons() +{ + if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) + { + CloseHandle(g_hRestrictedWeaponsConfig); + g_hRestrictedWeaponsConfig = INVALID_HANDLE; + } + + decl String:buffer[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, buffer, sizeof(buffer), FILE_RESTRICTEDWEAPONS); + new Handle:kv = CreateKeyValues("root"); + if (!FileToKeyValues(kv, buffer)) + { + CloseHandle(kv); + LogError("Failed to load restricted weapons list! File not found!"); + } + else + { + g_hRestrictedWeaponsConfig = kv; + LogSF2Message("Reloaded restricted weapons configuration file successfully"); + } +} + +public Action:Timer_RoundMessages(Handle:timer) +{ + if (!g_bEnabled) return Plugin_Stop; + + if (timer != g_hRoundMessagesTimer) return Plugin_Stop; + + switch (g_iRoundMessagesNum) + { + case 0: CPrintToChatAll("{olive}==== {lightgreen}Slender Fortress (%s){olive} coded by {lightgreen}Kit o' Rifty{olive} ====", PLUGIN_VERSION_DISPLAY); + case 1: CPrintToChatAll("%t", "SF2 Ad Message 1"); + case 2: CPrintToChatAll("%t", "SF2 Ad Message 2"); + } + + g_iRoundMessagesNum++; + if (g_iRoundMessagesNum > 2) g_iRoundMessagesNum = 0; + + return Plugin_Continue; +} + +public Action:Timer_WelcomeMessage(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + CPrintToChat(client, "%T", "SF2 Welcome Message", client); +} + +GetMaxPlayersForRound() +{ + new iOverride = GetConVarInt(g_cvMaxPlayersOverride); + if (iOverride != -1) return iOverride; + return GetConVarInt(g_cvMaxPlayers); +} + +public OnConVarChanged(Handle:cvar, const String:oldValue[], const String:newValue[]) +{ + if (cvar == g_cvDifficulty) + { + switch (StringToInt(newValue)) + { + case Difficulty_Easy: g_flRoundDifficultyModifier = DIFFICULTY_EASY; + case Difficulty_Hard: g_flRoundDifficultyModifier = DIFFICULTY_HARD; + case Difficulty_Insane: g_flRoundDifficultyModifier = DIFFICULTY_INSANE; + default: g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; + } + } + else if (cvar == g_cvMaxPlayers || cvar == g_cvMaxPlayersOverride) + { + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + CheckPlayerGroup(i); + } + } + else if (cvar == g_cvPlayerShakeEnabled) + { + g_bPlayerShakeEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobEnabled) + { + g_bPlayerViewbobEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobHurtEnabled) + { + g_bPlayerViewbobHurtEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobSprintEnabled) + { + g_bPlayerViewbobSprintEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvGravity) + { + g_flGravity = StringToFloat(newValue); + } + else if (cvar == g_cv20Dollars) + { + g_b20Dollars = bool:StringToInt(newValue); + } + else if (cvar == g_cvAllChat) + { + if (g_bEnabled) + { + for (new i = 1; i <= MaxClients; i++) + { + ClientUpdateListeningFlags(i); + } + } + } +} + +// ========================================================== +// IN-GAME AND ENTITY HOOK FUNCTIONS +// ========================================================== + + +public OnEntityCreated(ent, const String:classname[]) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(ent) || ent <= 0) return; + + if (StrEqual(classname, "spotlight_end", false)) + { + SDKHook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); + } + else if (StrEqual(classname, "beam", false)) + { + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightBeamSetTransmit); + } + + PvP_OnEntityCreated(ent, classname); +} + +public OnEntityDestroyed(ent) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(ent) || ent <= 0) return; + + decl String:sClassname[64]; + GetEntityClassname(ent, sClassname, sizeof(sClassname)); + + if (StrEqual(sClassname, "light_dynamic", false)) + { + AcceptEntityInput(ent, "TurnOff"); + + new iEnd = INVALID_ENT_REFERENCE; + while ((iEnd = FindEntityByClassname(iEnd, "spotlight_end")) != -1) + { + if (GetEntPropEnt(iEnd, Prop_Data, "m_hOwnerEntity") == ent) + { + AcceptEntityInput(iEnd, "Kill"); + break; + } + } + } + + PvP_OnEntityDestroyed(ent, sClassname); +} + +public Action:Hook_BlockUserMessage(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + if (!g_bEnabled) return Plugin_Continue; + return Plugin_Handled; +} + +public Action:Hook_NormalSound(clients[64], &numClients, String:sample[PLATFORM_MAX_PATH], &entity, &channel, &Float:volume, &level, &pitch, &flags) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsValidClient(entity)) + { + if (IsClientInGhostMode(entity)) + { + switch (channel) + { + case SNDCHAN_VOICE, SNDCHAN_WEAPON, SNDCHAN_ITEM, SNDCHAN_BODY: return Plugin_Handled; + } + } + else if (g_bPlayerProxy[entity]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[entity]); + if (iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + switch (channel) + { + case SNDCHAN_VOICE: + { + if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) + { + return Plugin_Handled; + } + } + } + } + } + else if (!g_bPlayerEliminated[entity]) + { + switch (channel) + { + case SNDCHAN_VOICE: + { + if (IsRoundInIntro()) return Plugin_Handled; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Voice)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDVOICE; + } + } + } + case SNDCHAN_BODY: + { + if (!StrContains(sample, "player/footsteps", false) || StrContains(sample, "step", false) != -1) + { + if (GetConVarBool(g_cvPlayerViewbobSprintEnabled) && IsClientReallySprinting(entity)) + { + // Viewpunch. + new Float:flPunchVelStep[3]; + + decl Float:flVelocity[3]; + GetEntPropVector(entity, Prop_Data, "m_vecAbsVelocity", flVelocity); + new Float:flSpeed = GetVectorLength(flVelocity); + + flPunchVelStep[0] = flSpeed / 300.0; + flPunchVelStep[1] = 0.0; + flPunchVelStep[2] = 0.0; + + ClientViewPunch(entity, flPunchVelStep); + } + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Footstep)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEP; + + if (IsClientSprinting(entity) && !(GetEntProp(entity, Prop_Send, "m_bDucking") || GetEntProp(entity, Prop_Send, "m_bDucked"))) + { + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEPLOUD; + } + } + } + } + } + case SNDCHAN_ITEM, SNDCHAN_WEAPON: + { + if (StrContains(sample, "impact", false) != -1 || StrContains(sample, "hit", false) != -1) + { + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Weapon)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDWEAPON; + } + } + } + } + } + } + } + + new bool:bModified = false; + + for (new i = 0; i < numClients; i++) + { + new iClient = clients[i]; + if (IsValidClient(iClient) && IsPlayerAlive(iClient) && !IsClientInGhostMode(iClient)) + { + new bool:bCanHearSound = true; + + if (IsValidClient(entity) && entity != iClient) + { + if (!g_bPlayerEliminated[iClient]) + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (!g_bPlayerEliminated[entity] && !DidClientEscape(entity)) + { + bCanHearSound = false; + } + } + } + } + + if (!bCanHearSound) + { + bModified = true; + clients[i] = -1; + } + } + } + + if (bModified) return Plugin_Changed; + return Plugin_Continue; +} + +public MRESReturn:Hook_EntityShouldTransmit(thisPointer, Handle:hReturn, Handle:hParams) +{ + if (!g_bEnabled) return MRES_Ignored; + + if (IsValidClient(thisPointer)) + { + if (DoesClientHaveConstantGlow(thisPointer)) + { + DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. + return MRES_Supercede; + } + } + else + { + new iBossIndex = NPCGetFromEntIndex(thisPointer); + if (iBossIndex != -1) + { + DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. + return MRES_Supercede; + } + } + + return MRES_Ignored; +} + +public Hook_TriggerOnStartTouch(const String:output[], caller, activator, Float:delay) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(caller)) return; + + decl String:sName[64]; + GetEntPropString(caller, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrContains(sName, "sf2_escape_trigger", false) == 0) + { + if (IsRoundInEscapeObjective()) + { + if (IsValidClient(activator) && IsPlayerAlive(activator) && !IsClientInDeathCam(activator) && !g_bPlayerEliminated[activator] && !DidClientEscape(activator)) + { + ClientEscape(activator); + TeleportClientToEscapePoint(activator); + } + } + } + + PvP_OnTriggerStartTouch(caller, activator); +} + +public Hook_TriggerOnEndTouch(const String:sOutput[], caller, activator, Float:flDelay) +{ + if (!g_bEnabled) return; + + PvP_OnTriggerEndTouch(caller, activator); +} + +public Action:Hook_PageOnTakeDamage(page, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsValidClient(attacker)) + { + if (!g_bPlayerEliminated[attacker]) + { + if (damagetype & 0x80) // 0x80 == melee damage + { + CollectPage(page, attacker); + } + } + } + + return Plugin_Continue; +} + +static CollectPage(page, activator) +{ + SetPageCount(g_iPageCount + 1); + g_iPlayerPageCount[activator] += 1; + EmitSoundToAll(PAGE_GRABSOUND, activator, SNDCHAN_ITEM, SNDLEVEL_SCREAMING); + + // Gives points. Credit to the makers of VSH/FF2. + new Handle:hEvent = CreateEvent("player_escort_score", true); + SetEventInt(hEvent, "player", activator); + SetEventInt(hEvent, "points", 1); + FireEvent(hEvent); + + AcceptEntityInput(page, "FireUser1"); + AcceptEntityInput(page, "Kill"); +} + +// ========================================================== +// GENERIC CLIENT HOOKS AND FUNCTIONS +// ========================================================== + + +public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2]) +{ + if (!g_bEnabled) return Plugin_Continue; + + ClientDisableFakeLagCompensation(client); + + // Check impulse (block spraying and built-in flashlight) + switch (impulse) + { + case 100: + { + impulse = 0; + } + case 201: + { + if (IsClientInGhostMode(client)) + { + impulse = 0; + } + } + } + + for (new i = 0; i < MAX_BUTTONS; i++) + { + new button = (1 << i); + + if ((buttons & button)) + { + if (!(g_iPlayerLastButtons[client] & button)) + { + ClientOnButtonPress(client, button); + } + } + else if ((g_iPlayerLastButtons[client] & button)) + { + ClientOnButtonRelease(client, button); + } + } + + g_iPlayerLastButtons[client] = buttons; + + return Plugin_Continue; +} + + +public OnClientCookiesCached(client) +{ + if (!g_bEnabled) return; + + // Load our saved settings. + new String:sCookie[64]; + GetClientCookie(client, g_hCookie, sCookie, sizeof(sCookie)); + + g_iPlayerQueuePoints[client] = 0; + + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; + g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = true; + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = true; + + if (sCookie[0]) + { + new String:s2[12][32]; + new count = ExplodeString(sCookie, " ; ", s2, 12, 32); + + if (count > 0) + g_iPlayerQueuePoints[client] = StringToInt(s2[0]); + if (count > 1) + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = bool:StringToInt(s2[1]); + if (count > 2) + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode:StringToInt(s2[2]); + if (count > 3) + g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = bool:StringToInt(s2[3]); + if (count > 4) + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = bool:StringToInt(s2[4]); + if (count > 5) + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = bool:StringToInt(s2[5]); + } +} + +public OnClientPutInServer(client) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientPutInServer(%d)", client); +#endif + + ClientSetPlayerGroup(client, -1); + + g_bPlayerEscaped[client] = false; + g_bPlayerEliminated[client] = true; + g_bPlayerChoseTeam[client] = false; + g_bPlayerPlayedSpecialRound[client] = true; + g_bPlayerPlayedNewBossRound[client] = true; + + g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn] = false; + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; + + g_iPlayerPageCount[client] = 0; + g_iPlayerDesiredFOV[client] = 90; + + SDKHook(client, SDKHook_PreThink, Hook_ClientPreThink); + SDKHook(client, SDKHook_SetTransmit, Hook_ClientSetTransmit); + SDKHook(client, SDKHook_OnTakeDamage, Hook_ClientOnTakeDamage); + + DHookEntity(g_hSDKWantsLagCompensationOnEntity, true, client); + DHookEntity(g_hSDKShouldTransmit, true, client); + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + + SetPlayerGroupInvitedPlayer(i, client, false); + SetPlayerGroupInvitedPlayerCount(i, client, 0); + SetPlayerGroupInvitedPlayerTime(i, client, 0.0); + } + + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientUpdateListeningFlags(client); + ClientUpdateMusicSystem(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + ClientResetProxy(client); + ClientResetHints(client); + ClientResetScare(client); + + ClientResetDeathCam(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientSetScareBoostEndTime(client, -1.0); + + ClientStartProxyAvailableTimer(client); + + if (!IsFakeClient(client)) + { + // See if the player is using the projected flashlight. + QueryClientConVar(client, "mat_supportflashlight", OnClientGetProjectedFlashlightSetting); + + // Get desired FOV. + QueryClientConVar(client, "fov_desired", OnClientGetDesiredFOV); + } + + PvP_OnClientPutInServer(client); + +#if defined DEBUG + g_iPlayerDebugFlags[client] = 0; + + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientPutInServer(%d)", client); +#endif +} + +public OnClientGetProjectedFlashlightSetting(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) +{ + if (result != ConVarQuery_Okay) + { + LogError("Warning: Player %N failed to query for ConVar mat_supportflashlight", client); + return; + } + + if (StringToInt(cvarValue)) + { + decl String:sAuth[64]; + GetClientAuthString(client, sAuth, sizeof(sAuth)); + + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = true; + LogSF2Message("Player %N (%s) has mat_supportflashlight enabled, projected flashlight will be used", client, sAuth); + } +} + +public OnClientGetDesiredFOV(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) +{ + if (!IsValidClient(client)) return; + + g_iPlayerDesiredFOV[client] = StringToInt(cvarValue); +} + +public OnClientDisconnect(client) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientDisconnect(%d)", client); +#endif + + g_bPlayerEscaped[client] = false; + + // Save and reset settings for the next client. + ClientSaveCookies(client); + ClientSetPlayerGroup(client, -1); + + // Reset variables. + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; + g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = true; + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; + + // Reset any client functions that may be still active. + ClientResetOverlay(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientSetGhostModeState(client, false); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientStopProxyForce(client); + + if (!IsRoundInWarmup()) + { + if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) + { + if (g_bRoundGrace) + { + // Force the next player in queue to take my place, if any. + ForceInNextPlayersInQueue(1, true); + } + else + { + if (!IsRoundEnding()) + { + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + } + + // Reset queue points global variable. + g_iPlayerQueuePoints[client] = 0; + + PvP_OnClientDisconnect(client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientDisconnect(%d)", client); +#endif +} + +public OnClientDisconnect_Post(client) +{ + g_iPlayerLastButtons[client] = 0; +} + +public TF2_OnWaitingForPlayersStart() +{ + g_bRoundWaitingForPlayers = true; +} + +public TF2_OnWaitingForPlayersEnd() +{ + g_bRoundWaitingForPlayers = false; +} + +SF2RoundState:GetRoundState() +{ + return g_iRoundState; +} + +SetRoundState(SF2RoundState:iRoundState) +{ + if (g_iRoundState == iRoundState) return; + + PrintToServer("SetRoundState(%d)", iRoundState); + + new SF2RoundState:iOldRoundState = GetRoundState(); + g_iRoundState = iRoundState; + + // Cleanup from old roundstate if needed. + switch (iOldRoundState) + { + case SF2RoundState_Waiting: + { + } + case SF2RoundState_Intro: + { + g_hRoundIntroTimer = INVALID_HANDLE; + } + case SF2RoundState_Active: + { + g_bRoundGrace = false; + g_hRoundGraceTimer = INVALID_HANDLE; + g_hRoundTimer = INVALID_HANDLE; + } + case SF2RoundState_Escape: + { + g_hRoundTimer = INVALID_HANDLE; + } + case SF2RoundState_Outro: + { + } + } + + switch (g_iRoundState) + { + case SF2RoundState_Waiting: + { + } + case SF2RoundState_Intro: + { + g_hRoundIntroTimer = INVALID_HANDLE; + g_iRoundIntroText = 0; + g_bRoundIntroTextDefault = false; + g_hRoundIntroTextTimer = CreateTimer(0.0, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hRoundIntroTextTimer); + + // Gather data on the intro parameters set by the map. + new Float:flHoldTime = g_flRoundIntroFadeHoldTime; + g_hRoundIntroTimer = CreateTimer(flHoldTime, Timer_ActivateRoundFromIntro, _, TIMER_FLAG_NO_MAPCHANGE); + + // Trigger any intro logic entities, if any. + new ent = -1; + while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) + { + decl String:sName[64]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_intro_relay", false)) + { + AcceptEntityInput(ent, "Trigger"); + break; + } + } + } + case SF2RoundState_Active: + { + // Start the grace period timer. + g_bRoundGrace = true; + g_hRoundGraceTimer = CreateTimer(GetConVarFloat(g_cvGraceTime), Timer_RoundGrace, _, TIMER_FLAG_NO_MAPCHANGE); + + CreateTimer(2.0, Timer_RoundStart, _, TIMER_FLAG_NO_MAPCHANGE); + + // Enable movement on players. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + SetEntityFlags(i, GetEntityFlags(i) & ~FL_FROZEN); + } + + // Fade in. + new Float:flFadeTime = g_flRoundIntroFadeDuration; + new iFadeFlags = SF_FADE_IN | FFADE_PURGE; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + UTIL_ScreenFade(i, FixedUnsigned16(flFadeTime, 1 << 12), 0, iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); + } + } + case SF2RoundState_Escape: + { + // Initialize the escape timer, if needed. + if (g_iRoundEscapeTimeLimit > 0) + { + g_iRoundTime = g_iRoundEscapeTimeLimit; + g_hRoundTimer = CreateTimer(1.0, Timer_RoundTimeEscape, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hRoundTimer = INVALID_HANDLE; + } + + decl String:sName[32]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_logic_escape", false)) + { + AcceptEntityInput(ent, "FireUser1"); + break; + } + } + } + case SF2RoundState_Outro: + { + if (!g_bRoundHasEscapeObjective) + { + // Teleport winning players to the escape point. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + TeleportClientToEscapePoint(i); + } + } + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (IsClientInGhostMode(i)) + { + // Take the player out of ghost mode. + ClientSetGhostModeState(i, false); + TF2_RespawnPlayer(i); + } + else if (g_bPlayerProxy[i]) + { + TF2_RespawnPlayer(i); + } + + if (!g_bPlayerEliminated[i]) + { + // Give them back all their weapons so they can beat the crap out of the other team. + TF2_RegeneratePlayer(i); + } + + ClientUpdateListeningFlags(i); + } + } + } +} + +bool:IsRoundInEscapeObjective() +{ + return bool:(GetRoundState() == SF2RoundState_Escape); +} + +bool:IsRoundInWarmup() +{ + return bool:(GetRoundState() == SF2RoundState_Waiting); +} + +bool:IsRoundInIntro() +{ + return bool:(GetRoundState() == SF2RoundState_Intro); +} + +bool:IsRoundEnding() +{ + return bool:(GetRoundState() == SF2RoundState_Outro); +} + +bool:IsInfiniteBlinkEnabled() +{ + return bool:(g_bRoundInfiniteBlink || (GetConVarInt(g_cvPlayerInfiniteBlinkOverride) == 1)); +} + +bool:IsInfiniteFlashlightEnabled() +{ + return bool:(g_bRoundInfiniteFlashlight || (GetConVarInt(g_cvPlayerInfiniteFlashlightOverride) == 1)); +} + +bool:IsInfiniteSprintEnabled() +{ + return bool:(g_bRoundInfiniteSprint || (GetConVarInt(g_cvPlayerInfiniteSprintOverride) == 1)); +} + + +#define SF2_PLAYER_HUD_BLINK_SYMBOL "B" +#define SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL "ϟ" +#define SF2_PLAYER_HUD_BAR_SYMBOL "|" +#define SF2_PLAYER_HUD_BAR_MISSING_SYMBOL "" +#define SF2_PLAYER_HUD_INFINITY_SYMBOL "∞" +#define SF2_PLAYER_HUD_SPRINT_SYMBOL "»" + +public Action:Timer_ClientAverageUpdate(Handle:timer) +{ + if (timer != g_hClientAverageUpdateTimer) return Plugin_Stop; + + if (!g_bEnabled) return Plugin_Stop; + + if (IsRoundInWarmup() || IsRoundEnding()) return Plugin_Continue; + + // First, process through HUD stuff. + decl String:buffer[256]; + + static iHudColorHealthy[3] = { 150, 255, 150 }; + static iHudColorCritical[3] = { 255, 10, 10 }; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (IsPlayerAlive(i) && !IsClientInDeathCam(i)) + { + if (!g_bPlayerEliminated[i]) + { + if (DidClientEscape(i)) continue; + + new iMaxBars = 12; + new iBars = RoundToCeil(float(iMaxBars) * ClientGetBlinkMeter(i)); + if (iBars > iMaxBars) iBars = iMaxBars; + + Format(buffer, sizeof(buffer), "%s ", SF2_PLAYER_HUD_BLINK_SYMBOL); + + if (IsInfiniteBlinkEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + + if (!g_bSpecialRound || g_iSpecialRoundType != SPECIALROUND_LIGHTSOUT) + { + iBars = RoundToCeil(float(iMaxBars) * ClientGetFlashlightBatteryLife(i)); + if (iBars > iMaxBars) iBars = iMaxBars; + + decl String:sBuffer2[64]; + Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL); + StrCat(buffer, sizeof(buffer), sBuffer2); + + if (IsInfiniteFlashlightEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + } + + iBars = RoundToCeil(float(iMaxBars) * (float(ClientGetSprintPoints(i)) / 100.0)); + if (iBars > iMaxBars) iBars = iMaxBars; + + decl String:sBuffer2[64]; + Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_SPRINT_SYMBOL); + StrCat(buffer, sizeof(buffer), sBuffer2); + + if (IsInfiniteSprintEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + + + new Float:flHealthRatio = float(GetEntProp(i, Prop_Send, "m_iHealth")) / float(SDKCall(g_hSDKGetMaxHealth, i)); + + new iColor[3]; + for (new i2 = 0; i2 < 3; i2++) + { + iColor[i2] = RoundFloat(float(iHudColorHealthy[i2]) + (float(iHudColorCritical[i2] - iHudColorHealthy[i2]) * (1.0 - flHealthRatio))); + } + + SetHudTextParams(0.035, 0.83, + 0.3, + iColor[0], + iColor[1], + iColor[2], + 40, + _, + 1.0, + 0.07, + 0.5); + ShowSyncHudText(i, g_hHudSync2, buffer); + } + else + { + if (g_bPlayerProxy[i]) + { + new iMaxBars = 12; + new iBars = RoundToCeil(float(iMaxBars) * (float(g_iPlayerProxyControl[i]) / 100.0)); + if (iBars > iMaxBars) iBars = iMaxBars; + + strcopy(buffer, sizeof(buffer), "CONTROL\n"); + + for (new i2 = 0; i2 < iBars; i2++) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + + SetHudTextParams(-1.0, 0.83, + 0.3, + SF2_HUD_TEXT_COLOR_R, + SF2_HUD_TEXT_COLOR_G, + SF2_HUD_TEXT_COLOR_B, + 40, + _, + 1.0, + 0.07, + 0.5); + ShowSyncHudText(i, g_hHudSync2, buffer); + } + } + } + + ClientUpdateListeningFlags(i); + ClientUpdateMusicSystem(i); + } + + return Plugin_Continue; +} + +stock bool:IsClientParticipating(client) +{ + if (!IsValidClient(client)) return false; + + if (bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) + { + // Who would coach in this game? + return false; + } + + new iTeam = GetClientTeam(client); + + if (g_bPlayerLagCompensation[client]) + { + iTeam = g_iPlayerLagCompensationTeam[client]; + } + + switch (iTeam) + { + case TFTeam_Unassigned, TFTeam_Spectator: return false; + } + + if (_:TF2_GetPlayerClass(client) == 0) + { + // Player hasn't chosen a class? What. + return false; + } + + return true; +} + +Handle:GetQueueList() +{ + new Handle:hArray = CreateArray(3); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientParticipating(i)) continue; + if (IsPlayerGroupActive(ClientGetPlayerGroup(i))) continue; + + new index = PushArrayCell(hArray, i); + SetArrayCell(hArray, index, g_iPlayerQueuePoints[i], 1); + SetArrayCell(hArray, index, false, 2); + } + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + new index = PushArrayCell(hArray, i); + SetArrayCell(hArray, index, GetPlayerGroupQueuePoints(i), 1); + SetArrayCell(hArray, index, true, 2); + } + + if (GetArraySize(hArray)) SortADTArrayCustom(hArray, SortQueueList); + return hArray; +} + +SetClientPlayState(client, bool:bState, bool:bEnablePlay=true) +{ + if (bState) + { + if (!g_bPlayerEliminated[client]) return; + + g_bPlayerEliminated[client] = false; + g_bPlayerPlaying[client] = bEnablePlay; + g_hPlayerSwitchBlueTimer[client] = INVALID_HANDLE; + + ClientSetGhostModeState(client, false); + + PvP_SetPlayerPvPState(client, false, false, false); + + if (g_bSpecialRound) + { + SetClientPlaySpecialRoundState(client, true); + } + + if (g_bNewBossRound) + { + SetClientPlayNewBossRoundState(client, true); + } + + if (TF2_GetPlayerClass(client) == TFClassType:0) + { + // Player hasn't chosen a class for some reason. Choose one for him. + TF2_SetPlayerClass(client, TFClassType:GetRandomInt(1, 9), true, true); + } + + ChangeClientTeamNoSuicide(client, _:TFTeam_Red); + } + else + { + if (g_bPlayerEliminated[client]) return; + + g_bPlayerEliminated[client] = true; + g_bPlayerPlaying[client] = false; + + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + } +} + +bool:DidClientPlayNewBossRound(client) +{ + return g_bPlayerPlayedNewBossRound[client]; +} + +SetClientPlayNewBossRoundState(client, bool:bState) +{ + g_bPlayerPlayedNewBossRound[client] = bState; +} + +bool:DidClientPlaySpecialRound(client) +{ + return g_bPlayerPlayedNewBossRound[client]; +} + +SetClientPlaySpecialRoundState(client, bool:bState) +{ + g_bPlayerPlayedSpecialRound[client] = bState; +} + +TeleportClientToEscapePoint(client) +{ + if (!IsClientInGame(client)) return; + + new ent = EntRefToEntIndex(g_iRoundEscapePointEntity); + if (ent && ent != -1) + { + decl Float:flPos[3], Float:flAng[3]; + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); + GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); + + TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); + AcceptEntityInput(ent, "FireUser1", client); + } +} + +ForceInNextPlayersInQueue(iAmount, bool:bShowMessage=false) +{ + // Grab the next person in line, or the next group in line if space allows. + new iAmountLeft = iAmount; + new Handle:hPlayers = CreateArray(); + new Handle:hArray = GetQueueList(); + + for (new i = 0, iSize = GetArraySize(hArray); i < iSize && iAmountLeft > 0; i++) + { + if (!GetArrayCell(hArray, i, 2)) + { + new iClient = GetArrayCell(hArray, i); + if (g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; + + PushArrayCell(hPlayers, iClient); + iAmountLeft--; + } + else + { + new iGroupIndex = GetArrayCell(hArray, i); + if (!IsPlayerGroupActive(iGroupIndex)) continue; + + new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); + if (iMemberCount <= iAmountLeft) + { + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient) || g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; + if (ClientGetPlayerGroup(iClient) == iGroupIndex) + { + PushArrayCell(hPlayers, iClient); + } + } + + SetPlayerGroupPlaying(iGroupIndex, true); + + iAmountLeft -= iMemberCount; + } + } + } + + CloseHandle(hArray); + + for (new i = 0, iSize = GetArraySize(hPlayers); i < iSize; i++) + { + new iClient = GetArrayCell(hPlayers, i); + ClientSetQueuePoints(iClient, 0); + SetClientPlayState(iClient, true); + + if (bShowMessage) CPrintToChat(iClient, "%T", "SF2 Force Play", iClient); + } + + CloseHandle(hPlayers); +} + +public SortQueueList(index1, index2, Handle:array, Handle:hndl) +{ + new iQueuePoints1 = GetArrayCell(array, index1, 1); + new iQueuePoints2 = GetArrayCell(array, index2, 1); + + if (iQueuePoints1 > iQueuePoints2) return -1; + else if (iQueuePoints1 == iQueuePoints2) return 0; + return 1; +} + +// ========================================================== +// GENERIC PAGE/BOSS HOOKS AND FUNCTIONS +// ========================================================== + +public Action:Hook_SlenderObjectSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) + { + if (!IsValidEdict(GetEntPropEnt(other, Prop_Send, "m_hObserverTarget"))) return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action:Timer_SlenderBlinkBossThink(Handle:timer, any:entref) +{ + new slender = EntRefToEntIndex(entref); + if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return Plugin_Stop; + + if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (NPCGetType(iBossIndex) == SF2BossType_Creeper) + { + new bool:bMove = false; + + if ((GetGameTime() - g_flSlenderLastKill[iBossIndex]) >= GetProfileFloat(sProfile, "kill_cooldown")) + { + if (PeopleCanSeeSlender(iBossIndex, false, false) && !PeopleCanSeeSlender(iBossIndex, true, SlenderUsesBlink(iBossIndex))) + { + new iBestPlayer = -1; + new Handle:hArray = CreateArray(); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInDeathCam(i) || g_bPlayerEliminated[i] || DidClientEscape(i) || IsClientInGhostMode(i) || !PlayerCanSeeSlender(i, iBossIndex, false, false)) continue; + PushArrayCell(hArray, i); + } + + if (GetArraySize(hArray)) + { + decl Float:flSlenderPos[3]; + SlenderGetAbsOrigin(iBossIndex, flSlenderPos); + + decl Float:flTempPos[3]; + new iTempPlayer = -1; + new Float:flTempDist = 16384.0; + for (new i = 0; i < GetArraySize(hArray); i++) + { + new iClient = GetArrayCell(hArray, i); + GetClientAbsOrigin(iClient, flTempPos); + if (GetVectorDistance(flTempPos, flSlenderPos) < flTempDist) + { + iTempPlayer = iClient; + flTempDist = GetVectorDistance(flTempPos, flSlenderPos); + } + } + + iBestPlayer = iTempPlayer; + } + + CloseHandle(hArray); + + decl Float:buffer[3]; + if (iBestPlayer != -1 && SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, buffer)) + { + bMove = true; + + decl Float:flAng[3], Float:flBuffer[3]; + decl Float:flSlenderPos[3], Float:flPos[3]; + GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flSlenderPos); + GetClientAbsOrigin(iBestPlayer, flPos); + SubtractVectors(flPos, buffer, flAng); + GetVectorAngles(flAng, flAng); + + // Take care of angle offsets. + AddVectors(flAng, g_flSlenderEyeAngOffset[iBossIndex], flAng); + for (new i = 0; i < 3; i++) flAng[i] = AngleNormalize(flAng[i]); + + flAng[0] = 0.0; + + // Take care of position offsets. + GetProfileVector(sProfile, "pos_offset", flBuffer); + AddVectors(buffer, flBuffer, buffer); + + TeleportEntity(slender, buffer, flAng, NULL_VECTOR); + + new Float:flMaxRange = GetProfileFloat(sProfile, "teleport_range_max"); + new Float:flDist = GetVectorDistance(buffer, flPos); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + + if (flDist < (flMaxRange * 0.33)) + { + GetProfileString(sProfile, "model_closedist", sBuffer, sizeof(sBuffer)); + } + else if (flDist < (flMaxRange * 0.66)) + { + GetProfileString(sProfile, "model_averagedist", sBuffer, sizeof(sBuffer)); + } + else + { + GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); + } + + // Fallback if error. + if (!sBuffer[0]) GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); + + SetEntProp(slender, Prop_Send, "m_nModelIndex", PrecacheModel(sBuffer)); + + if (flDist <= NPCGetInstantKillRadius(iBossIndex)) + { + if (NPCGetFlags(iBossIndex) & SFF_FAKE) + { + SlenderMarkAsFake(iBossIndex); + return Plugin_Stop; + } + else + { + g_flSlenderLastKill[iBossIndex] = GetGameTime(); + ClientStartDeathCam(iBestPlayer, iBossIndex, buffer); + } + } + } + } + } + + if (bMove) + { + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_move_single", sBuffer, sizeof(sBuffer)); + if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + + GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); + if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING, SND_CHANGEVOL); + } + else + { + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); + if (sBuffer[0]) StopSound(slender, SNDCHAN_AUTO, sBuffer); + } + } + + return Plugin_Continue; +} + + +SlenderOnClientStressUpdate(client) +{ + new Float:flStress = g_flPlayerStress[client]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + new iBossFlags = NPCGetFlags(iBossIndex); + if (iBossFlags & SFF_MARKEDASFAKE || + iBossFlags & SFF_NOTELEPORT) + { + continue; + } + + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); + if (iTeleportTarget && iTeleportTarget != INVALID_ENT_REFERENCE) + { + if (g_bPlayerEliminated[iTeleportTarget] || + DidClientEscape(iTeleportTarget) || + flStress >= g_flSlenderTeleportMaxTargetStress[iBossIndex] || + GetGameTime() >= g_flSlenderTeleportMaxTargetTime[iBossIndex]) + { + // Queue for a new target and mark the old target in the rest period. + new Float:flRestPeriod = GetProfileFloat(sProfile, "teleport_target_rest_period", 15.0); + flRestPeriod = (flRestPeriod * GetRandomFloat(0.92, 1.08)) / (NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier); + + g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderTeleportPlayersRestTime[iBossIndex][iTeleportTarget] = GetGameTime() + flRestPeriod; + g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; + g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: lost target, putting at rest period", iBossIndex); +#endif + } + } + else if (!g_bRoundGrace) + { + new iPreferredTeleportTarget = INVALID_ENT_REFERENCE; + + new Float:flTargetStressMin = GetProfileFloat(sProfile, "teleport_target_stress_min", 0.2); + new Float:flTargetStressMax = GetProfileFloat(sProfile, "teleport_target_stress_max", 0.9); + + new Float:flTargetStress = flTargetStressMax - ((flTargetStressMax - flTargetStressMin) / (g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex))); + + new Float:flPreferredTeleportTargetStress = flTargetStress; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + IsClientInGhostMode(i) || + DidClientEscape(i)) + { + continue; + } + + if (g_flPlayerStress[i] < flPreferredTeleportTargetStress) + { + if (g_flSlenderTeleportPlayersRestTime[iBossIndex][i] <= GetGameTime()) + { + iPreferredTeleportTarget = i; + flPreferredTeleportTargetStress = g_flPlayerStress[i]; + } + } + } + + if (iPreferredTeleportTarget && iPreferredTeleportTarget != INVALID_ENT_REFERENCE) + { + // Set our preferred target to the new guy. + new Float:flTargetDuration = GetProfileFloat(sProfile, "teleport_target_persistency_period", 13.0); + new Float:flDeviation = GetRandomFloat(0.92, 1.08); + flTargetDuration = Pow(flDeviation * flTargetDuration, ((g_flRoundDifficultyModifier * (NPCGetAnger(iBossIndex) - 1.0)) / 2.0)) + ((flDeviation * flTargetDuration) - 1.0); + + g_iSlenderTeleportTarget[iBossIndex] = EntIndexToEntRef(iPreferredTeleportTarget); + g_flSlenderTeleportPlayersRestTime[iBossIndex][iPreferredTeleportTarget] = -1.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = GetGameTime() + flTargetDuration; + g_flSlenderTeleportTargetTime[iBossIndex] = GetGameTime(); + g_flSlenderTeleportMaxTargetStress[iBossIndex] = flTargetStress; + + iTeleportTarget = iPreferredTeleportTarget; + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: got new target %N", iBossIndex, iPreferredTeleportTarget); +#endif + } + } + } +} + +static GetPageMusicRanges() +{ + ClearArray(g_hPageMusicRanges); + + decl String:sName[64]; + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (sName[0] && !StrContains(sName, "sf2_page_music_", false)) + { + ReplaceString(sName, sizeof(sName), "sf2_page_music_", "", false); + + new String:sPageRanges[2][32]; + ExplodeString(sName, "-", sPageRanges, 2, 32); + + new iIndex = PushArrayCell(g_hPageMusicRanges, EntIndexToEntRef(ent)); + if (iIndex != -1) + { + new iMin = StringToInt(sPageRanges[0]); + new iMax = StringToInt(sPageRanges[1]); + +#if defined DEBUG + DebugMessage("Page range found: entity %d, iMin = %d, iMax = %d", ent, iMin, iMax); +#endif + SetArrayCell(g_hPageMusicRanges, iIndex, iMin, 1); + SetArrayCell(g_hPageMusicRanges, iIndex, iMax, 2); + } + } + } + + // precache + if (GetArraySize(g_hPageMusicRanges) > 0) + { + decl String:sPath[PLATFORM_MAX_PATH]; + + for (new i = 0; i < GetArraySize(g_hPageMusicRanges); i++) + { + ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + PrecacheSound(sPath); + } + } + } + + LogSF2Message("Loaded page music ranges successfully!"); +} + +SetPageCount(iNum) +{ + if (iNum > g_iPageMax) iNum = g_iPageMax; + + new iOldPageCount = g_iPageCount; + g_iPageCount = iNum; + + if (g_iPageCount != iOldPageCount) + { + if (g_iPageCount > iOldPageCount) + { + if (g_hRoundGraceTimer != INVALID_HANDLE) + { + TriggerTimer(g_hRoundGraceTimer); + } + + g_iRoundTime += g_iRoundTimeGainFromPage; + if (g_iRoundTime > g_iRoundTimeLimit) g_iRoundTime = g_iRoundTimeLimit; + + // Increase anger on selected bosses. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new Float:flPageDiff = NPCGetAngerAddOnPageGrabTimeDiff(i); + if (flPageDiff >= 0.0) + { + new iDiff = g_iPageCount - iOldPageCount; + if ((GetGameTime() - g_flPageFoundLastTime) < flPageDiff) + { + NPCAddAnger(i, NPCGetAngerAddOnPageGrab(i) * float(iDiff)); + } + } + } + + g_flPageFoundLastTime = GetGameTime(); + } + + // Notify logic entities. + decl String:sTargetName[64]; + decl String:sFindTargetName[64]; + Format(sFindTargetName, sizeof(sFindTargetName), "sf2_onpagecount_%d", g_iPageCount); + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); + if (sTargetName[0] && StrEqual(sTargetName, sFindTargetName, false)) + { + AcceptEntityInput(ent, "Trigger"); + break; + } + } + + new iClients[MAXPLAYERS + 1] = { -1, ... }; + new iClientsNum = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (!g_bPlayerEliminated[i] || IsClientInGhostMode(i)) + { + if (g_iPageCount) + { + iClients[iClientsNum] = i; + iClientsNum++; + } + } + } + + if (g_iPageCount > 0 && g_bRoundHasEscapeObjective && g_iPageCount == g_iPageMax) + { + // Escape initialized! + SetRoundState(SF2RoundState_Escape); + + if (iClientsNum) + { + new iGameTextEscape = GetTextEntity("sf2_escape_message", false); + if (iGameTextEscape != -1) + { + // Custom escape message. + decl String:sMessage[512]; + GetEntPropString(iGameTextEscape, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextEscape, g_hHudSync, sMessage); + } + else + { + // Default escape message. + for (new i = 0; i < iClientsNum; i++) + { + new client = iClients[i]; + ClientShowMainMessage(client, "%d/%d\n%T", g_iPageCount, g_iPageMax, "SF2 Default Escape Message", i); + } + } + } + } + else + { + if (iClientsNum) + { + new iGameTextPage = GetTextEntity("sf2_page_message", false); + if (iGameTextPage != -1) + { + // Custom page message. + decl String:sMessage[512]; + GetEntPropString(iGameTextPage, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextPage, g_hHudSync, sMessage, g_iPageCount, g_iPageMax); + } + else + { + // Default page message. + for (new i = 0; i < iClientsNum; i++) + { + new client = iClients[i]; + ClientShowMainMessage(client, "%d/%d", g_iPageCount, g_iPageMax); + } + } + } + } + + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + } +} + +GetTextEntity(const String:sTargetName[], bool:bCaseSensitive=true) +{ + // Try to see if we can use a custom message instead of the default. + decl String:targetName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "game_text")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (StrEqual(targetName, sTargetName, bCaseSensitive)) + { + return ent; + } + } + } + + return -1; +} + +ShowHudTextUsingTextEntity(const iClients[], iClientsNum, iGameText, Handle:hHudSync, const String:sMessage[], ...) +{ + if (!sMessage[0]) return; + if (!IsValidEntity(iGameText)) return; + + decl String:sTrueMessage[512]; + VFormat(sTrueMessage, sizeof(sTrueMessage), sMessage, 6); + + new Float:flX = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.x"); + new Float:flY = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.y"); + new iEffect = GetEntProp(iGameText, Prop_Data, "m_textParms.effect"); + new Float:flFadeInTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime"); + new Float:flFadeOutTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime"); + new Float:flHoldTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); + new Float:flFxTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fxTime"); + + new Color1[4] = { 255, 255, 255, 255 }; + new Color2[4] = { 255, 255, 255, 255 }; + + new iParmsOffset = FindDataMapOffs(iGameText, "m_textParms"); + if (iParmsOffset != -1) + { + // hudtextparms_s m_textParms + + Color1[0] = GetEntData(iGameText, iParmsOffset + 12, 1); + Color1[1] = GetEntData(iGameText, iParmsOffset + 13, 1); + Color1[2] = GetEntData(iGameText, iParmsOffset + 14, 1); + Color1[3] = GetEntData(iGameText, iParmsOffset + 15, 1); + + Color2[0] = GetEntData(iGameText, iParmsOffset + 16, 1); + Color2[1] = GetEntData(iGameText, iParmsOffset + 17, 1); + Color2[2] = GetEntData(iGameText, iParmsOffset + 18, 1); + Color2[3] = GetEntData(iGameText, iParmsOffset + 19, 1); + } + + SetHudTextParamsEx(flX, flY, flHoldTime, Color1, Color2, iEffect, flFxTime, flFadeInTime, flFadeOutTime); + + for (new i = 0; i < iClientsNum; i++) + { + new iClient = iClients[i]; + if (!IsValidClient(iClient) || IsFakeClient(iClient)) continue; + + ShowSyncHudText(iClient, hHudSync, sTrueMessage); + } +} + +// ========================================================== +// EVENT HOOKS +// ========================================================== + +public Event_RoundStart(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundStart"); +#endif + + // Reset some global variables. + g_iRoundCount++; + g_hRoundTimer = INVALID_HANDLE; + + SetRoundState(SF2RoundState_Invalid); + + SetPageCount(0); + g_iPageMax = 0; + g_flPageFoundLastTime = GetGameTime(); + + g_hVoteTimer = INVALID_HANDLE; + + // Remove all bosses from the game. + NPCRemoveAll(); + + // Refresh groups. + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + SetPlayerGroupPlaying(i, false); + CheckPlayerGroup(i); + } + + // Refresh players. + for (new i = 1; i <= MaxClients; i++) + { + ClientSetGhostModeState(i, false); + + g_bPlayerPlaying[i] = false; + g_bPlayerEliminated[i] = true; + g_bPlayerEscaped[i] = false; + } + + // Calculate the new round state. + if (g_bRoundWaitingForPlayers) + { + SetRoundState(SF2RoundState_Waiting); + } + else if (GetConVarBool(g_cvWarmupRound) && g_iRoundWarmupRoundCount < GetConVarInt(g_cvWarmupRoundNum)) + { + g_iRoundWarmupRoundCount++; + + SetRoundState(SF2RoundState_Waiting); + + ServerCommand("mp_restartgame 15"); + PrintCenterTextAll("Round restarting in 15 seconds"); + } + else + { + g_iRoundActiveCount++; + + InitializeNewGame(); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundStart"); +#endif +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundEnd"); +#endif + + SetRoundState(SF2RoundState_Outro); + + DistributeQueuePointsToPlayers(); + + g_iRoundEndCount++; + CheckRoundLimitForBossPackVote(g_iRoundEndCount); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundEnd"); +#endif +} + +static DistributeQueuePointsToPlayers() +{ + // Give away queue points. + new iDefaultAmount = 5; + new iAmount = iDefaultAmount; + new iAmount2 = iAmount; + new Action:iAction = Plugin_Continue; + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + + if (IsPlayerGroupPlaying(i)) + { + SetPlayerGroupQueuePoints(i, 0); + } + else + { + iAmount = iDefaultAmount; + iAmount2 = iAmount; + iAction = Plugin_Continue; + + Call_StartForward(fOnGroupGiveQueuePoints); + Call_PushCell(i); + Call_PushCellRef(iAmount2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) iAmount = iAmount2; + + SetPlayerGroupQueuePoints(i, GetPlayerGroupQueuePoints(i) + iAmount); + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient)) continue; + if (ClientGetPlayerGroup(iClient) == i) + { + CPrintToChat(iClient, "%T", "SF2 Give Group Queue Points", iClient, iAmount); + } + } + } + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (g_bPlayerPlaying[i]) + { + ClientSetQueuePoints(i, 0); + } + else + { + if (!IsClientParticipating(i)) + { + CPrintToChat(i, "%T", "SF2 No Queue Points To Spectator", i); + } + else + { + iAmount = iDefaultAmount; + iAmount2 = iAmount; + iAction = Plugin_Continue; + + Call_StartForward(fOnClientGiveQueuePoints); + Call_PushCell(i); + Call_PushCellRef(iAmount2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) iAmount = iAmount2; + + ClientSetQueuePoints(i, g_iPlayerQueuePoints[i] + iAmount); + CPrintToChat(i, "%T", "SF2 Give Queue Points", i, iAmount); + } + } + } +} + +public Action:Event_PlayerTeamPre(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerTeamPre"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + if (GetEventInt(event, "team") > 1 || GetEventInt(event, "oldteam") > 1) SetEventBroadcast(event, true); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerTeamPre"); +#endif + + return Plugin_Continue; +} + +public Event_PlayerTeam(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerTeam"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + new iNewTeam = GetEventInt(event, "team"); + if (iNewTeam <= _:TFTeam_Spectator) + { + if (g_bRoundGrace) + { + if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) + { + ForceInNextPlayersInQueue(1, true); + } + } + + // You're not playing anymore. + if (g_bPlayerPlaying[client]) + { + ClientSetQueuePoints(client, 0); + } + + g_bPlayerPlaying[client] = false; + g_bPlayerEliminated[client] = true; + g_bPlayerEscaped[client] = false; + + ClientSetGhostModeState(client, false); + + if (!bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) + { + // This is to prevent player spawn spam when someone is coaching. Who coaches in SF2, anyway? + TF2_RespawnPlayer(client); + } + + // Special round. + if (g_bSpecialRound) g_bPlayerPlayedSpecialRound[client] = true; + + // Boss round. + if (g_bNewBossRound) g_bPlayerPlayedNewBossRound[client] = true; + } + else + { + if (!g_bPlayerChoseTeam[client]) + { + g_bPlayerChoseTeam[client] = true; + + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + EmitSoundToClient(client, SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); + CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Projected{olive}."); + } + else + { + CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Normal{olive}."); + } + + CreateTimer(5.0, Timer_WelcomeMessage, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + } + + // Check groups. + if (!IsRoundEnding()) + { + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + CheckPlayerGroup(i); + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerTeam"); +#endif + +} + +/** + * Sets the player to the correct team if needed. Returns true if a change was necessary, false if no change occurred. + */ +static bool:HandlePlayerTeam(client, bool:bRespawn=true) +{ + if (!IsClientInGame(client) || !IsClientParticipating(client)) return false; + + if (!g_bPlayerEliminated[client]) + { + if (GetClientTeam(client) != _:TFTeam_Red) + { + if (bRespawn) + ChangeClientTeamNoSuicide(client, _:TFTeam_Red); + else + ChangeClientTeam(client, _:TFTeam_Red); + + return true; + } + } + else + { + if (GetClientTeam(client) != _:TFTeam_Blue) + { + if (bRespawn) + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + else + ChangeClientTeam(client, _:TFTeam_Blue); + + return true; + } + } + + return false; +} + +static HandlePlayerIntroState(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client) || !IsClientParticipating(client)) return; + + if (!IsRoundInIntro()) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START HandlePlayerIntroState(%d)", client); +#endif + + // Disable movement on player. + SetEntityFlags(client, GetEntityFlags(client) | FL_FROZEN); + + new Float:flDelay = 0.0; + if (!IsFakeClient(client)) + { + flDelay = GetClientLatency(client, NetFlow_Outgoing); + } + + CreateTimer(flDelay * 4.0, Timer_IntroBlackOut, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END HandlePlayerIntroState(%d)", client); +#endif +} + +HandlePlayerHUD(client) +{ + if (IsRoundInWarmup() || IsClientInGhostMode(client)) + { + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + else + { + if (!g_bPlayerEliminated[client]) + { + if (!DidClientEscape(client)) + { + // Player is in the game; disable normal HUD. + SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); + } + else + { + // Player isn't in the game; enable normal HUD behavior. + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + } + else + { + if (g_bPlayerProxy[client]) + { + // Player is in the game; disable normal HUD. + SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); + } + else + { + // Player isn't in the game; enable normal HUD behavior. + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + } + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerSpawn(%d)", client); +#endif + + if (!IsClientParticipating(client)) + { + ClientSetGhostModeState(client, false); + } + + g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; + + if (IsPlayerAlive(client) && IsClientParticipating(client)) + { + if (HandlePlayerTeam(client)) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("client->HandlePlayerTeam()"); +#endif + } + else + { + g_iPlayerPageCount[client] = 0; + + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientUpdateListeningFlags(client); + ClientUpdateMusicSystem(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + ClientResetProxy(client); + ClientResetHints(client); + ClientResetScare(client); + + ClientResetDeathCam(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientHandleGhostMode(client); + + if (!g_bPlayerEliminated[client]) + { + ClientStartDrainingBlinkMeter(client); + ClientSetScareBoostEndTime(client, -1.0); + + ClientStartCampingTimer(client); + + HandlePlayerIntroState(client); + + // screen overlay timer + g_hPlayerOverlayCheck[client] = CreateTimer(0.0, Timer_PlayerOverlayCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerOverlayCheck[client], true); + + if (DidClientEscape(client)) + { + CreateTimer(0.1, Timer_TeleportPlayerToEscapePoint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + ClientEnableConstantGlow(client, "head"); + ClientActivateUltravision(client); + } + } + else + { + g_hPlayerOverlayCheck[client] = INVALID_HANDLE; + } + + g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + HandlePlayerHUD(client); + } + } + + PvP_OnPlayerSpawn(client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerSpawn(%d)", client); +#endif +} + +public Action:Timer_IntroBlackOut(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!IsRoundInIntro()) return; + + if (!IsPlayerAlive(client) || g_bPlayerEliminated[client]) return; + + // Black out the player's screen. + new iFadeFlags = FFADE_OUT | FFADE_STAYOUT | FFADE_PURGE; + UTIL_ScreenFade(client, 0, FixedUnsigned16(90.0, 1 << 12), iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); +} + +public Event_PostInventoryApplication(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PostInventoryApplication"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PostInventoryApplication"); +#endif +} + +public Action:Event_DontBroadcastToClients(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsRoundInWarmup()) return Plugin_Continue; + + SetEventBroadcast(event, true); + return Plugin_Continue; +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerDeathPre"); +#endif + + if (!IsRoundInWarmup()) + { + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + if (!IsRoundEnding()) + { + if (g_bRoundGrace || g_bPlayerEliminated[client] || IsClientInGhostMode(client)) + { + SetEventBroadcast(event, true); + } + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerDeathPre"); +#endif + + return Plugin_Continue; +} + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerHurt"); +#endif + + ClientDisableFakeLagCompensation(client); + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (attacker > 0) + { + if (g_bPlayerProxy[attacker]) + { + g_iPlayerProxyControl[attacker] = 100; + } + } + + // Play any sounds, if any. + if (g_bPlayerProxy[client]) + { + new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iProxyMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + if (GetRandomStringFromProfile(sProfile, "sound_proxy_hurt", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_hurt_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_hurt_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_hurt_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_hurt_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_hurt_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerHurt"); +#endif +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerDeath(%d)", client); +#endif + + new bool:bFake = bool:(GetEventInt(event, "death_flags") & TF_DEATHFLAG_DEADRINGER); + new inflictor = GetEventInt(event, "inflictor_entindex"); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("inflictor = %d", inflictor); +#endif + + if (!bFake) + { + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetDeathCam(client); + + ClientUpdateMusicSystem(client); + + PvP_SetPlayerPvPState(client, false, false, false); + + if (IsRoundInWarmup()) + { + CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + if (!g_bPlayerEliminated[client]) + { + if (IsRoundInIntro() || g_bRoundGrace || DidClientEscape(client)) + { + CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_bPlayerEliminated[client] = true; + g_bPlayerEscaped[client] = false; + g_hPlayerSwitchBlueTimer[client] = CreateTimer(0.5, Timer_PlayerSwitchToBlue, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + else + { + } + + { + // If this player was killed by a boss, play a sound. + new npcIndex = NPCGetFromEntIndex(inflictor); + if (npcIndex != -1) + { + decl String:npcProfile[SF2_MAX_PROFILE_NAME_LENGTH], String:buffer[PLATFORM_MAX_PATH]; + NPCGetProfile(npcIndex, npcProfile, sizeof(npcProfile)); + + if (GetRandomStringFromProfile(npcProfile, "sound_attack_killed_all", buffer, sizeof(buffer)) && strlen(buffer) > 0) + { + if (!g_bPlayerEliminated[client]) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_HELICOPTER); + } + } + + SlenderPerformVoice(npcIndex, "sound_attack_killed"); + } + } + + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + + // Notify to other bosses that this player has died. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (EntRefToEntIndex(g_iSlenderTarget[i]) == client) + { + g_iSlenderInterruptConditions[i] |= COND_CHASETARGETINVALIDATED; + GetClientAbsOrigin(client, g_flSlenderChaseDeathPosition[i]); + } + } + } + + if (g_bPlayerProxy[client]) + { + // We're a proxy, so play some sounds. + + new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iProxyMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + if (GetRandomStringFromProfile(sProfile, "sound_proxy_death", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_death_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_death_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_death_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_death_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_death_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } + + ClientResetProxy(client, false); + ClientUpdateListeningFlags(client); + + // Half-Zatoichi nerf code. + new iKatanaHealthGain = GetConVarInt(g_cvHalfZatoichiHealthGain); + if (iKatanaHealthGain >= 0) + { + new iAttacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (iAttacker > 0) + { + if (!IsClientInPvP(iAttacker) && (!g_bPlayerEliminated[iAttacker] || g_bPlayerProxy[iAttacker])) + { + decl String:sWeapon[64]; + GetEventString(event, "weapon", sWeapon, sizeof(sWeapon)); + + if (StrEqual(sWeapon, "demokatana")) + { + new iAttackerPreHealth = GetEntProp(iAttacker, Prop_Send, "m_iHealth"); + new Handle:hPack = CreateDataPack(); + WritePackCell(hPack, GetClientUserId(iAttacker)); + WritePackCell(hPack, iAttackerPreHealth + iKatanaHealthGain); + + CreateTimer(0.0, Timer_SetPlayerHealth, hPack, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + } + + g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; + } + + PvP_OnPlayerDeath(client, bFake); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerDeath(%d)", client); +#endif +} + +public Action:Timer_SetPlayerHealth(Handle:timer, any:data) +{ + new Handle:hPack = Handle:data; + ResetPack(hPack); + new iAttacker = GetClientOfUserId(ReadPackCell(hPack)); + new iHealth = ReadPackCell(hPack); + CloseHandle(hPack); + + if (iAttacker <= 0) return; + + SetEntProp(iAttacker, Prop_Data, "m_iHealth", iHealth); + SetEntProp(iAttacker, Prop_Send, "m_iHealth", iHealth); +} + +public Action:Timer_PlayerSwitchToBlue(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSwitchBlueTimer[client]) return; + + ChangeClientTeam(client, _:TFTeam_Blue); +} + +public Action:Timer_RoundStart(Handle:timer) +{ + if (g_iPageMax > 0) + { + new Handle:hArrayClients = CreateArray(); + new iClients[MAXPLAYERS + 1]; + new iClientsNum = 0; + + new iGameText = GetTextEntity("sf2_intro_message", false); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || g_bPlayerEliminated[i]) continue; + + if (iGameText == -1) + { + if (g_iPageMax > 1) + { + ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Plural", i, g_iPageMax); + } + else + { + ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Singular", i, g_iPageMax); + } + } + + PushArrayCell(hArrayClients, GetClientUserId(i)); + iClients[iClientsNum] = i; + iClientsNum++; + } + + // Show difficulty menu. + if (iClientsNum) + { + // Automatically set it to Normal. + SetConVarInt(g_cvDifficulty, Difficulty_Normal); + + g_hVoteTimer = CreateTimer(1.0, Timer_VoteDifficulty, hArrayClients, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hVoteTimer, true); + + if (iGameText != -1) + { + decl String:sMessage[512]; + GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); + } + } + else + { + CloseHandle(hArrayClients); + } + } +} + +public Action:Timer_CheckRoundWinConditions(Handle:timer) +{ + CheckRoundWinConditions(); +} + +public Action:Timer_RoundGrace(Handle:timer) +{ + if (timer != g_hRoundGraceTimer) return; + + g_bRoundGrace = false; + g_hRoundGraceTimer = INVALID_HANDLE; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientParticipating(i)) g_bPlayerEliminated[i] = true; + } + + // Initialize the main round timer. + if (g_iRoundTimeLimit > 0) + { + // Set round time. + g_iRoundTime = g_iRoundTimeLimit; + g_hRoundTimer = CreateTimer(1.0, Timer_RoundTime, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + else + { + // Infinite round time. + g_hRoundTimer = INVALID_HANDLE; + } + + CPrintToChatAll("{olive}%t", "SF2 Grace Period End"); +} + +public Action:Timer_RoundTime(Handle:timer) +{ + if (timer != g_hRoundTimer) return Plugin_Stop; + + if (g_iRoundTime <= 0) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; + + decl Float:flBuffer[3]; + GetClientAbsOrigin(i, flBuffer); + SDKHooks_TakeDamage(i, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + } + + return Plugin_Stop; + } + + g_iRoundTime--; + + new hours, minutes, seconds; + FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); + + SetHudTextParams(-1.0, 0.1, + 1.0, + SF2_HUD_TEXT_COLOR_R, SF2_HUD_TEXT_COLOR_G, SF2_HUD_TEXT_COLOR_B, SF2_HUD_TEXT_COLOR_A, + _, + _, + 1.5, 1.5); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; + ShowSyncHudText(i, g_hRoundTimerSync, "%d/%d\n%d:%02d", g_iPageCount, g_iPageMax, minutes, seconds); + } + + return Plugin_Continue; +} + +public Action:Timer_RoundTimeEscape(Handle:timer) +{ + if (timer != g_hRoundTimer) return Plugin_Stop; + + if (g_iRoundTime <= 0) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i) || DidClientEscape(i)) continue; + + decl Float:flBuffer[3]; + GetClientAbsOrigin(i, flBuffer); + ClientStartDeathCam(i, 0, flBuffer); + } + + return Plugin_Stop; + } + + new hours, minutes, seconds; + FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); + + SetHudTextParams(-1.0, 0.1, + 1.0, + SF2_HUD_TEXT_COLOR_R, + SF2_HUD_TEXT_COLOR_G, + SF2_HUD_TEXT_COLOR_B, + SF2_HUD_TEXT_COLOR_A, + _, + _, + 1.5, 1.5); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; + ShowSyncHudText(i, g_hRoundTimerSync, "%T\n%d:%02d", "SF2 Default Escape Message", i, minutes, seconds); + } + + g_iRoundTime--; + + return Plugin_Continue; +} + +public Action:Timer_VoteDifficulty(Handle:timer, any:data) +{ + new Handle:hArrayClients = Handle:data; + + if (timer != g_hVoteTimer || IsRoundEnding()) + { + CloseHandle(hArrayClients); + return Plugin_Stop; + } + + if (IsVoteInProgress()) return Plugin_Continue; // There's another vote in progess. Wait. + + new iClients[MAXPLAYERS + 1] = { -1, ... }; + new iClientsNum; + for (new i = 0, iSize = GetArraySize(hArrayClients); i < iSize; i++) + { + new iClient = GetClientOfUserId(GetArrayCell(hArrayClients, i)); + if (iClient <= 0) continue; + + iClients[iClientsNum] = iClient; + iClientsNum++; + } + + CloseHandle(hArrayClients); + + VoteMenu(g_hMenuVoteDifficulty, iClients, iClientsNum, 15); + + return Plugin_Stop; +} + +static InitializeMapEntities() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeMapEntities()"); +#endif + + g_bRoundInfiniteFlashlight = false; + g_bRoundInfiniteBlink = false; + g_bRoundInfiniteSprint = false; + g_bRoundHasEscapeObjective = false; + + g_iRoundTimeLimit = GetConVarInt(g_cvTimeLimit); + g_iRoundEscapeTimeLimit = GetConVarInt(g_cvTimeLimitEscape); + g_iRoundTimeGainFromPage = GetConVarInt(g_cvTimeGainFromPageGrab); + + // Reset page reference. + g_bPageRef = false; + strcopy(g_strPageRefModel, sizeof(g_strPageRefModel), ""); + g_flPageRefModelScale = 1.0; + + new Handle:hArray = CreateArray(2); + new Handle:hPageTrie = CreateTrie(); + + decl String:targetName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (!StrContains(targetName, "sf2_maxpages_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_maxpages_", "", false); + g_iPageMax = StringToInt(targetName); + } + else if (!StrContains(targetName, "sf2_page_spawnpoint", false)) + { + if (!StrContains(targetName, "sf2_page_spawnpoint_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_page_spawnpoint_", "", false); + if (targetName[0]) + { + new Handle:hButtStallion = INVALID_HANDLE; + if (!GetTrieValue(hPageTrie, targetName, hButtStallion)) + { + hButtStallion = CreateArray(); + SetTrieValue(hPageTrie, targetName, hButtStallion); + } + + new iIndex = FindValueInArray(hArray, hButtStallion); + if (iIndex == -1) + { + iIndex = PushArrayCell(hArray, hButtStallion); + } + + PushArrayCell(hButtStallion, ent); + SetArrayCell(hArray, iIndex, true, 1); + } + else + { + new iIndex = PushArrayCell(hArray, ent); + SetArrayCell(hArray, iIndex, false, 1); + } + } + else + { + new iIndex = PushArrayCell(hArray, ent); + SetArrayCell(hArray, iIndex, false, 1); + } + } + else if (!StrContains(targetName, "sf2_logic_escape", false)) + { + g_bRoundHasEscapeObjective = true; + } + else if (!StrContains(targetName, "sf2_infiniteflashlight", false)) + { + g_bRoundInfiniteFlashlight = true; + } + else if (!StrContains(targetName, "sf2_infiniteblink", false)) + { + g_bRoundInfiniteBlink = true; + } + else if (!StrContains(targetName, "sf2_infinitesprint", false)) + { + g_bRoundInfiniteSprint = true; + } + else if (!StrContains(targetName, "sf2_time_limit_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_time_limit_", "", false); + g_iRoundTimeLimit = StringToInt(targetName); + + LogSF2Message("Found sf2_time_limit entity, set time limit to %d", g_iRoundTimeLimit); + } + else if (!StrContains(targetName, "sf2_escape_time_limit_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_escape_time_limit_", "", false); + g_iRoundEscapeTimeLimit = StringToInt(targetName); + + LogSF2Message("Found sf2_escape_time_limit entity, set escape time limit to %d", g_iRoundEscapeTimeLimit); + } + else if (!StrContains(targetName, "sf2_time_gain_from_page_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_time_gain_from_page_", "", false); + g_iRoundTimeGainFromPage = StringToInt(targetName); + + LogSF2Message("Found sf2_time_gain_from_page entity, set time gain to %d", g_iRoundTimeGainFromPage); + } + else if (g_iRoundActiveCount == 1 && (!StrContains(targetName, "sf2_maxplayers_", false))) + { + ReplaceString(targetName, sizeof(targetName), "sf2_maxplayers_", "", false); + SetConVarInt(g_cvMaxPlayers, StringToInt(targetName)); + + LogSF2Message("Found sf2_maxplayers entity, set maxplayers to %d", StringToInt(targetName)); + } + else if (!StrContains(targetName, "sf2_boss_override_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_boss_override_", "", false); + SetConVarString(g_cvBossProfileOverride, targetName); + + LogSF2Message("Found sf2_boss_override entity, set boss profile override to %s", targetName); + } + } + } + + // Get a reference entity, if any. + + ent = -1; + while ((ent = FindEntityByClassname(ent, "prop_dynamic")) != -1) + { + if (g_bPageRef) break; + + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (StrEqual(targetName, "sf2_page_model", false)) + { + g_bPageRef = true; + GetEntPropString(ent, Prop_Data, "m_ModelName", g_strPageRefModel, sizeof(g_strPageRefModel)); + g_flPageRefModelScale = 1.0; + } + } + } + + new iPageCount = GetArraySize(hArray); + if (iPageCount) + { + SortADTArray(hArray, Sort_Random, Sort_Integer); + + decl Float:vecPos[3], Float:vecAng[3], Float:vecDir[3]; + decl page; + ent = -1; + + for (new i = 0; i < iPageCount && (i + 1) <= g_iPageMax; i++) + { + if (bool:GetArrayCell(hArray, i, 1)) + { + new Handle:hButtStallion = Handle:GetArrayCell(hArray, i); + ent = GetArrayCell(hButtStallion, GetRandomInt(0, GetArraySize(hButtStallion) - 1)); + } + else + { + ent = GetArrayCell(hArray, i); + } + + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", vecPos); + GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", vecAng); + GetAngleVectors(vecAng, vecDir, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(vecDir, vecDir); + ScaleVector(vecDir, 1.0); + + page = CreateEntityByName("prop_dynamic_override"); + if (page != -1) + { + TeleportEntity(page, vecPos, vecAng, NULL_VECTOR); + DispatchKeyValue(page, "targetname", "sf2_page"); + + if (g_bPageRef) + { + SetEntityModel(page, g_strPageRefModel); + } + else + { + SetEntityModel(page, PAGE_MODEL); + } + + DispatchKeyValue(page, "solid", "2"); + DispatchSpawn(page); + ActivateEntity(page); + SetVariantInt(i); + AcceptEntityInput(page, "Skin"); + AcceptEntityInput(page, "EnableCollision"); + + if (g_bPageRef) + { + SetEntPropFloat(page, Prop_Send, "m_flModelScale", g_flPageRefModelScale); + } + else + { + SetEntPropFloat(page, Prop_Send, "m_flModelScale", PAGE_MODELSCALE); + } + + SDKHook(page, SDKHook_OnTakeDamage, Hook_PageOnTakeDamage); + SDKHook(page, SDKHook_SetTransmit, Hook_SlenderObjectSetTransmit); + } + } + + // Safely remove all handles. + for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) + { + if (bool:GetArrayCell(hArray, i, 1)) + { + CloseHandle(Handle:GetArrayCell(hArray, i)); + } + } + + Call_StartForward(fOnPagesSpawned); + Call_Finish(); + } + + CloseHandle(hPageTrie); + CloseHandle(hArray); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeMapEntities()"); +#endif +} + +static HandleSpecialRoundState() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleSpecialRoundState()"); +#endif + + new bool:bOld = g_bSpecialRound; + new bool:bContinuousOld = g_bSpecialRoundContinuous; + g_bSpecialRound = false; + g_bSpecialRoundNew = false; + g_bSpecialRoundContinuous = false; + + new bool:bForceNew = false; + + if (bOld) + { + if (bContinuousOld) + { + // Check if there are players who haven't played the special round yet. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedSpecialRound[i] = true; + continue; + } + + if (!g_bPlayerPlayedSpecialRound[i]) + { + // Someone didn't get to play this yet. Continue the special round. + g_bSpecialRound = true; + g_bSpecialRoundContinuous = true; + break; + } + } + } + } + + new iRoundInterval = GetConVarInt(g_cvSpecialRoundInterval); + + if (iRoundInterval > 0 && g_iSpecialRoundCount >= iRoundInterval) + { + g_bSpecialRound = true; + bForceNew = true; + } + + // Do special round force override and reset it. + if (GetConVarInt(g_cvSpecialRoundForce) >= 0) + { + g_bSpecialRound = GetConVarBool(g_cvSpecialRoundForce); + SetConVarInt(g_cvSpecialRoundForce, -1); + } + + if (g_bSpecialRound) + { + if (bForceNew || !bOld || !bContinuousOld) + { + g_bSpecialRoundNew = true; + } + + if (g_bSpecialRoundNew) + { + if (GetConVarInt(g_cvSpecialRoundBehavior) == 1) + { + g_bSpecialRoundContinuous = true; + } + else + { + // New special round, but it's not continuous. + g_bSpecialRoundContinuous = false; + } + } + } + else + { + g_bSpecialRoundContinuous = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleSpecialRoundState() -> g_bSpecialRound = %d (count = %d, new = %d, continuous = %d)", g_bSpecialRound, g_iSpecialRoundCount, g_bSpecialRoundNew, g_bSpecialRoundContinuous); +#endif +} + +bool:IsNewBossRoundRunning() +{ + return g_bNewBossRound; +} + +/** + * Returns an array which contains all the profile names valid to be chosen for a new boss round. + */ +static Handle:GetNewBossRoundProfileList() +{ + new Handle:hBossList = CloneArray(GetSelectableBossProfileList()); + + if (GetArraySize(hBossList) > 0) + { + decl String:sMainBoss[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossMain, sMainBoss, sizeof(sMainBoss)); + + new index = FindStringInArray(hBossList, sMainBoss); + if (index != -1) + { + // Main boss exists; remove him from the list. + RemoveFromArray(hBossList, index); + } + else + { + // Main boss doesn't exist; remove the first boss from the list. + RemoveFromArray(hBossList, 0); + } + } + + return hBossList; +} + +static HandleNewBossRoundState() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleNewBossRoundState()"); +#endif + + new bool:bOld = g_bNewBossRound; + new bool:bContinuousOld = g_bNewBossRoundContinuous; + g_bNewBossRound = false; + g_bNewBossRoundNew = false; + g_bNewBossRoundContinuous = false; + + new bool:bForceNew = false; + + if (bOld) + { + if (bContinuousOld) + { + // Check if there are players who haven't played the boss round yet. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedNewBossRound[i] = true; + continue; + } + + if (!g_bPlayerPlayedNewBossRound[i]) + { + // Someone didn't get to play this yet. Continue the boss round. + g_bNewBossRound = true; + g_bNewBossRoundContinuous = true; + break; + } + } + } + } + + // Don't force a new special round while a continuous round is going on. + if (!g_bNewBossRoundContinuous) + { + new iRoundInterval = GetConVarInt(g_cvNewBossRoundInterval); + + if (/*iRoundInterval > 0 &&*/ iRoundInterval <= 0 || g_iNewBossRoundCount >= iRoundInterval) + { + g_bNewBossRound = true; + bForceNew = true; + } + } + + // Do boss round force override and reset it. + if (GetConVarInt(g_cvNewBossRoundForce) >= 0) + { + g_bNewBossRound = GetConVarBool(g_cvNewBossRoundForce); + SetConVarInt(g_cvNewBossRoundForce, -1); + } + + // Check if we have enough bosses. + if (g_bNewBossRound) + { + new Handle:hBossList = GetNewBossRoundProfileList(); + + if (GetArraySize(hBossList) < 1) + { + g_bNewBossRound = false; // Not enough bosses. + } + + CloseHandle(hBossList); + } + + if (g_bNewBossRound) + { + if (bForceNew || !bOld || !bContinuousOld) + { + g_bNewBossRoundNew = true; + } + + if (g_bNewBossRoundNew) + { + if (GetConVarInt(g_cvNewBossRoundBehavior) == 1) + { + g_bNewBossRoundContinuous = true; + } + else + { + // New "new boss round", but it's not continuous. + g_bNewBossRoundContinuous = false; + } + } + } + else + { + g_bNewBossRoundContinuous = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleNewBossRoundState() -> g_bNewBossRound = %d (count = %d, new = %d, continuous = %d)", g_bNewBossRound, g_iNewBossRoundCount, g_bNewBossRoundNew, g_bNewBossRoundContinuous); +#endif +} + +/** + * Returns the amount of players that are in game and currently not eliminated. + */ +GetActivePlayerCount() +{ + new count = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + count++; + } + } + + return count; +} + +static SelectStartingBossesForRound() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START SelectStartingBossesForRound()"); +#endif + + new Handle:hSelectableBossList = GetSelectableBossProfileList(); + + // Select which boss profile to use. + decl String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); + + if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) + { + // Pick the overridden boss. + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfileOverride); + SetConVarString(g_cvBossProfileOverride, ""); + } + else if (g_bNewBossRound) + { + if (g_bNewBossRoundNew) + { + new Handle:hBossList = GetNewBossRoundProfileList(); + + GetArrayString(hBossList, GetRandomInt(0, GetArraySize(hBossList) - 1), g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile)); + + CloseHandle(hBossList); + } + + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), g_strNewBossRoundProfile); + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossMain, sProfile, sizeof(sProfile)); + + if (strlen(sProfile) > 0 && IsProfileValid(sProfile)) + { + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfile); + } + else + { + if (GetArraySize(hSelectableBossList) > 0) + { + // Pick the first boss in our array if the main boss doesn't exist. + GetArrayString(hSelectableBossList, 0, g_strRoundBossProfile, sizeof(g_strRoundBossProfile)); + } + else + { + // No bosses to pick. What? + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), ""); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END SelectStartingBossesForRound() -> boss: %s", g_strRoundBossProfile); +#endif +} + +static GetRoundIntroParameters() +{ + g_iRoundIntroFadeColor[0] = 0; + g_iRoundIntroFadeColor[1] = 0; + g_iRoundIntroFadeColor[2] = 0; + g_iRoundIntroFadeColor[3] = 255; + + g_flRoundIntroFadeHoldTime = GetConVarFloat(g_cvIntroDefaultHoldTime); + g_flRoundIntroFadeDuration = GetConVarFloat(g_cvIntroDefaultFadeTime); + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "env_fade")) != -1) + { + decl String:sName[32]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_intro_fade", false)) + { + new iColorOffset = FindSendPropOffs("CBaseEntity", "m_clrRender"); + if (iColorOffset != -1) + { + g_iRoundIntroFadeColor[0] = GetEntData(ent, iColorOffset, 1); + g_iRoundIntroFadeColor[1] = GetEntData(ent, iColorOffset + 1, 1); + g_iRoundIntroFadeColor[2] = GetEntData(ent, iColorOffset + 2, 1); + g_iRoundIntroFadeColor[3] = GetEntData(ent, iColorOffset + 3, 1); + } + + g_flRoundIntroFadeHoldTime = GetEntPropFloat(ent, Prop_Data, "m_HoldTime"); + g_flRoundIntroFadeDuration = GetEntPropFloat(ent, Prop_Data, "m_Duration"); + + break; + } + } + + // Get the intro music. + strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), SF2_INTRO_DEFAULT_MUSIC); + + ent = -1; + while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) + { + decl String:sName[64]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrEqual(sName, "sf2_intro_music", false)) + { + decl String:sSongPath[PLATFORM_MAX_PATH]; + GetEntPropString(ent, Prop_Data, "m_iszSound", sSongPath, sizeof(sSongPath)); + + if (strlen(sSongPath) == 0) + { + LogError("Found sf2_intro_music entity, but it has no sound path specified! Default intro music will be used instead."); + } + else + { + strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), sSongPath); + } + + break; + } + } +} + +static GetRoundEscapeParameters() +{ + g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; + + decl String:sName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (!StrContains(sName, "sf2_escape_spawnpoint", false)) + { + g_iRoundEscapePointEntity = EntIndexToEntRef(ent); + break; + } + } +} + +InitializeNewGame() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeNewGame()"); +#endif + + GetRoundIntroParameters(); + GetRoundEscapeParameters(); + + // Choose round state. + if (GetConVarBool(g_cvIntroEnabled)) + { + // Set the round state to the intro stage. + SetRoundState(SF2RoundState_Intro); + } + else + { + SetRoundState(SF2RoundState_Active); + } + + if (g_iRoundActiveCount == 1) + { + SetConVarString(g_cvBossProfileOverride, ""); + } + + HandleSpecialRoundState(); + + // Was a new special round initialized? + if (g_bSpecialRound) + { + if (g_bSpecialRoundNew) + { + // Reset round count. + g_iSpecialRoundCount = 1; + + if (g_bSpecialRoundContinuous) + { + // It's the start of a continuous special round. + + // Initialize all players' values. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedSpecialRound[i] = true; + continue; + } + + g_bPlayerPlayedSpecialRound[i] = false; + } + } + + SpecialRoundCycleStart(); + } + else + { + SpecialRoundStart(); + + if (g_bSpecialRoundContinuous) + { + // Display the current special round going on to late players. + CreateTimer(3.0, Timer_DisplaySpecialRound, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + else + { + g_iSpecialRoundCount++; + + SpecialRoundReset(); + } + + // Determine boss round state. + HandleNewBossRoundState(); + + if (g_bNewBossRound) + { + if (g_bNewBossRoundNew) + { + // Reset round count; + g_iNewBossRoundCount = 1; + + if (g_bNewBossRoundContinuous) + { + // It's the start of a continuous "new boss round". + + // Initialize all players' values. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedNewBossRound[i] = true; + continue; + } + + g_bPlayerPlayedNewBossRound[i] = false; + } + } + } + } + else + { + g_iNewBossRoundCount++; + } + + InitializeMapEntities(); + + // Initialize pages and entities. + GetPageMusicRanges(); + + SelectStartingBossesForRound(); + + ForceInNextPlayersInQueue(GetMaxPlayersForRound()); + + // Respawn all players, if needed. + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientParticipating(i)) + { + if (!HandlePlayerTeam(i)) + { + if (!g_bPlayerEliminated[i]) + { + // Players currently in the "game" still have to be respawned. + TF2_RespawnPlayer(i); + } + } + } + } + + if (GetRoundState() == SF2RoundState_Intro) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + if (!IsFakeClient(i)) + { + // Currently in intro state, play intro music. + g_hPlayerIntroMusicTimer[i] = CreateTimer(0.5, Timer_PlayIntroMusicToPlayer, GetClientUserId(i), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; + } + } + else + { + g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; + } + } + } + else + { + // Spawn the boss! + SelectProfile(0, g_strRoundBossProfile); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeNewGame()"); +#endif +} + +public Action:Timer_PlayIntroMusicToPlayer(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerIntroMusicTimer[client]) return; + + g_hPlayerIntroMusicTimer[client] = INVALID_HANDLE; + + EmitSoundToClient(client, g_strRoundIntroMusic, _, MUSIC_CHAN, SNDLEVEL_NONE); +} + +public Action:Timer_IntroTextSequence(Handle:timer) +{ + if (!g_bEnabled) return; + if (g_hRoundIntroTextTimer != timer) return; + + new Float:flDuration = 0.0; + + if (g_iRoundIntroText != 0) + { + new bool:bFoundGameText = false; + + new iClients[MAXPLAYERS + 1]; + new iClientsNum; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + + iClients[iClientsNum] = i; + iClientsNum++; + } + + if (!g_bRoundIntroTextDefault) + { + decl String:sTargetname[64]; + Format(sTargetname, sizeof(sTargetname), "sf2_intro_text_%d", g_iRoundIntroText); + + new iGameText = FindEntityByTargetname(sTargetname, "game_text"); + if (iGameText && iGameText != INVALID_ENT_REFERENCE) + { + bFoundGameText = true; + flDuration = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); + + decl String:sMessage[512]; + GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); + } + } + else + { + if (g_iRoundIntroText == 2) + { + bFoundGameText = false; + + decl String:sMessage[64]; + GetCurrentMap(sMessage, sizeof(sMessage)); + + for (new i = 0; i < iClientsNum; i++) + { + ClientShowMainMessage(iClients[i], sMessage, 1); + } + } + } + + if (g_iRoundIntroText == 1 && !bFoundGameText) + { + // Use default intro sequence. Eugh. + g_bRoundIntroTextDefault = true; + flDuration = GetConVarFloat(g_cvIntroDefaultHoldTime) / 2.0; + + for (new i = 0; i < iClientsNum; i++) + { + EmitSoundToClient(iClients[i], SF2_INTRO_DEFAULT_MUSIC, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + else + { + if (!bFoundGameText) return; // done with sequence; don't check anymore. + } + } + + g_iRoundIntroText++; + g_hRoundIntroTextTimer = CreateTimer(flDuration, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ActivateRoundFromIntro(Handle:timer) +{ + if (!g_bEnabled) return; + if (g_hRoundIntroTimer != timer) return; + + // Obviously we don't want to spawn the boss when g_strRoundBossProfile isn't set yet. + SetRoundState(SF2RoundState_Active); + + // Spawn the boss! + SelectProfile(0, g_strRoundBossProfile); +} + +CheckRoundWinConditions() +{ + if (IsRoundInWarmup() || IsRoundEnding()) return; + + new iTotalCount = 0; + new iAliveCount = 0; + new iEscapedCount = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + iTotalCount++; + if (!g_bPlayerEliminated[i] && !IsClientInDeathCam(i)) + { + iAliveCount++; + if (DidClientEscape(i)) iEscapedCount++; + } + } + + if (iAliveCount == 0) + { + ForceTeamWin(_:TFTeam_Blue); + } + else + { + if (g_bRoundHasEscapeObjective) + { + if (iEscapedCount == iAliveCount) + { + ForceTeamWin(_:TFTeam_Red); + } + } + else + { + if (g_iPageMax > 0 && g_iPageCount == g_iPageMax) + { + ForceTeamWin(_:TFTeam_Red); + } + } + } +} + +// ========================================================== +// API +// ========================================================== + +public Native_IsRunning(Handle:plugin, numParams) +{ + return g_bEnabled; +} + +public Native_GetCurrentDifficulty(Handle:plugin, numParams) +{ + return GetConVarInt(g_cvDifficulty); +} + +public Native_GetDifficultyModifier(Handle:plugin, numParams) +{ + new iDifficulty = GetNativeCell(1); + if (iDifficulty < Difficulty_Easy || iDifficulty >= Difficulty_Max) + { + LogError("Difficulty parameter can only be from %d to %d!", Difficulty_Easy, Difficulty_Max - 1); + return _:1.0; + } + + switch (iDifficulty) + { + case Difficulty_Easy: return _:DIFFICULTY_EASY; + case Difficulty_Hard: return _:DIFFICULTY_HARD; + case Difficulty_Insane: return _:DIFFICULTY_INSANE; + } + + return _:DIFFICULTY_NORMAL; +} + +public Native_IsClientEliminated(Handle:plugin, numParams) +{ + return g_bPlayerEliminated[GetNativeCell(1)]; +} + +public Native_IsClientInGhostMode(Handle:plugin, numParams) +{ + return IsClientInGhostMode(GetNativeCell(1)); +} + +public Native_IsClientProxy(Handle:plugin, numParams) +{ + return g_bPlayerProxy[GetNativeCell(1)]; +} + +public Native_GetClientBlinkCount(Handle:plugin, numParams) +{ + return ClientGetBlinkCount(GetNativeCell(1)); +} + +public Native_GetClientProxyMaster(Handle:plugin, numParams) +{ + return NPCGetFromUniqueID(g_iPlayerProxyMaster[GetNativeCell(1)]); +} + +public Native_GetClientProxyControlAmount(Handle:plugin, numParams) +{ + return g_iPlayerProxyControl[GetNativeCell(1)]; +} + +public Native_GetClientProxyControlRate(Handle:plugin, numParams) +{ + return _:g_flPlayerProxyControlRate[GetNativeCell(1)]; +} + +public Native_SetClientProxyMaster(Handle:plugin, numParams) +{ + g_iPlayerProxyMaster[GetNativeCell(1)] = NPCGetUniqueID(GetNativeCell(2)); +} + +public Native_SetClientProxyControlAmount(Handle:plugin, numParams) +{ + g_iPlayerProxyControl[GetNativeCell(1)] = GetNativeCell(2); +} + +public Native_SetClientProxyControlRate(Handle:plugin, numParams) +{ + g_flPlayerProxyControlRate[GetNativeCell(1)] = Float:GetNativeCell(2); +} + +public Native_IsClientLookingAtBoss(Handle:plugin, numParams) +{ + return g_bPlayerSeesSlender[GetNativeCell(1)][GetNativeCell(2)]; +} + +public Native_CollectAsPage(Handle:plugin, numParams) +{ + CollectPage(GetNativeCell(1), GetNativeCell(2)); +} + +public Native_GetMaxBosses(Handle:plugin, numParams) +{ + return MAX_BOSSES; +} + +public Native_EntIndexToBossIndex(Handle:plugin, numParams) +{ + return NPCGetFromEntIndex(GetNativeCell(1)); +} + +public Native_BossIndexToEntIndex(Handle:plugin, numParams) +{ + return NPCGetEntIndex(GetNativeCell(1)); +} + +public Native_BossIDToBossIndex(Handle:plugin, numParams) +{ + return NPCGetFromUniqueID(GetNativeCell(1)); +} + +public Native_BossIndexToBossID(Handle:plugin, numParams) +{ + return NPCGetUniqueID(GetNativeCell(1)); +} + +public Native_GetBossName(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(GetNativeCell(1), sProfile, sizeof(sProfile)); + + SetNativeString(2, sProfile, GetNativeCell(3)); +} + +public Native_GetBossModelEntity(Handle:plugin, numParams) +{ + return EntRefToEntIndex(g_iSlenderModel[GetNativeCell(1)]); +} + +public Native_GetBossTarget(Handle:plugin, numParams) +{ + return EntRefToEntIndex(g_iSlenderTarget[GetNativeCell(1)]); +} + +public Native_GetBossMaster(Handle:plugin, numParams) +{ + return g_iSlenderCopyMaster[GetNativeCell(1)]; +} + +public Native_GetBossState(Handle:plugin, numParams) +{ + return g_iSlenderState[GetNativeCell(1)]; +} + +public Native_IsBossProfileValid(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + return IsProfileValid(sProfile); +} + +public Native_GetBossProfileNum(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + return GetProfileNum(sProfile, sKeyValue, GetNativeCell(3)); +} + +public Native_GetBossProfileFloat(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + return _:GetProfileFloat(sProfile, sKeyValue, Float:GetNativeCell(3)); +} + +public Native_GetBossProfileString(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + new iResultLen = GetNativeCell(4); + decl String:sResult[iResultLen]; + + decl String:sDefaultValue[512]; + GetNativeString(5, sDefaultValue, sizeof(sDefaultValue)); + + new bool:bSuccess = GetProfileString(sProfile, sKeyValue, sResult, iResultLen, sDefaultValue); + + SetNativeString(3, sResult, iResultLen); + return bSuccess; +} + +public Native_GetBossProfileVector(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + decl Float:flResult[3]; + decl Float:flDefaultValue[3]; + GetNativeArray(4, flDefaultValue, 3); + + new bool:bSuccess = GetProfileVector(sProfile, sKeyValue, flResult, flDefaultValue); + + SetNativeArray(3, flResult, 3); + return bSuccess; +} + +public Native_GetRandomStringFromBossProfile(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + new iBufferLen = GetNativeCell(4); + decl String:sBuffer[iBufferLen]; + + new iIndex = GetNativeCell(5); + + new bool:bSuccess = GetRandomStringFromProfile(sProfile, sKeyValue, sBuffer, iBufferLen, iIndex); + SetNativeString(3, sBuffer, iBufferLen); + return bSuccess; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/adminmenu.sp b/addons/sourcemod/scripting/rytp_horror/adminmenu.sp index f40582c..3b21732 100644 --- a/addons/sourcemod/scripting/rytp_horror/adminmenu.sp +++ b/addons/sourcemod/scripting/rytp_horror/adminmenu.sp @@ -1,911 +1,911 @@ -#if defined _sf2_adminmenu_included - #endinput -#endif -#define _sf2_adminmenu_included - -static Handle:g_hTopMenu = INVALID_HANDLE; -static g_iPlayerAdminMenuTargetUserId[MAXPLAYERS + 1] = { -1, ... }; - -SetupAdminMenu() -{ - /* Account for late loading */ - new Handle:hTopMenu = INVALID_HANDLE; - if (LibraryExists("adminmenu") && ((hTopMenu = GetAdminTopMenu()) != INVALID_HANDLE)) - { - OnAdminMenuReady(hTopMenu); - } -} - - -public OnAdminMenuReady(Handle:hTopMenu) -{ - if (hTopMenu == g_hTopMenu) return; - - g_hTopMenu = hTopMenu; - - new TopMenuObject:hServerCommands = FindTopMenuCategory(hTopMenu, ADMINMENU_SERVERCOMMANDS); - if (hServerCommands != INVALID_TOPMENUOBJECT) - { - AddToTopMenu(hTopMenu, "sf2_boss_admin_main", TopMenuObject_Item, AdminTopMenu_BossMain, hServerCommands, "sm_sf2_add_boss", ADMFLAG_SLAY); - } - - new TopMenuObject:hPlayerCommands = FindTopMenuCategory(hTopMenu, ADMINMENU_PLAYERCOMMANDS); - if (hPlayerCommands != INVALID_TOPMENUOBJECT) - { - AddToTopMenu(hTopMenu, "sf2_player_setplaystate", TopMenuObject_Item, AdminTopMenu_PlayerSetPlayState, hPlayerCommands, "sm_sf2_setplaystate", ADMFLAG_SLAY); - AddToTopMenu(hTopMenu, "sf2_player_force_proxy", TopMenuObject_Item, AdminTopMenu_PlayerForceProxy, hPlayerCommands, "sm_sf2_force_proxy", ADMFLAG_SLAY); - } -} - -static DisplayPlayerForceProxyAdminMenu(client) -{ - new Handle:hMenu = CreateMenu(AdminMenu_PlayerForceProxy); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy", client); - AddTargetsToMenu(hMenu, client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public AdminTopMenu_PlayerForceProxy(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) -{ - if (action == TopMenuAction_DisplayOption) - { - Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy", param); - } - else if (action == TopMenuAction_SelectOption) - { - DisplayPlayerForceProxyAdminMenu(param); - } -} - -public AdminMenu_PlayerForceProxy(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) - { - DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); - } - } - else if (action == MenuAction_Select) - { - decl String:sUserId[64]; - GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); - - new client = GetClientOfUserId(StringToInt(sUserId)); - if (client <= 0) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); - DisplayPlayerForceProxyAdminMenu(param1); - } - else - { - g_iPlayerAdminMenuTargetUserId[param1] = StringToInt(sUserId); - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - - new Handle:hMenu = CreateMenu(AdminMenu_PlayerForceProxyBoss); - if (!AddBossTargetsToMenu(hMenu)) - { - CloseHandle(hMenu); - DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", param1); - } - else - { - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy Boss", param1, sName); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); - } - } - } -} - -public AdminMenu_PlayerForceProxyBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayPlayerForceProxyAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - new client = GetClientOfUserId(g_iPlayerAdminMenuTargetUserId[param1]); - if (client <= 0) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); - } - else - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); - - if (!bool:GetProfileNum(sProfile, "proxies", 0) || - g_iSlenderCopyMaster[iIndex] != -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Not Allowed To Have Proxies", param1); - } - else if (!g_bPlayerEliminated[client]) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player In Game", param1); - } - else if (g_bPlayerProxy[param1]) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Already A Proxy", param1); - } - else - { - FakeClientCommand(param1, "sm_sf2_force_proxy #%d %d", g_iPlayerAdminMenuTargetUserId[param1], iIndex); - } - } - } - - DisplayPlayerForceProxyAdminMenu(param1); - } -} - -static DisplayPlayerSetPlayStateAdminMenu(client) -{ - new Handle:hMenu = CreateMenu(AdminMenu_PlayerSetPlayState); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Set Play State", client); - AddTargetsToMenu(hMenu, client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public AdminTopMenu_PlayerSetPlayState(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) -{ - if (action == TopMenuAction_DisplayOption) - { - Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Player Set Play State", param); - } - else if (action == TopMenuAction_SelectOption) - { - DisplayPlayerSetPlayStateAdminMenu(param); - } -} - -public AdminMenu_PlayerSetPlayState(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) - { - DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); - } - } - else if (action == MenuAction_Select) - { - decl String:sUserId[64]; - GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); - new client = GetClientOfUserId(StringToInt(sUserId)); - if (client <= 0) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); - DisplayPlayerSetPlayStateAdminMenu(param1); - } - else - { - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - - new Handle:hMenu = CreateMenu(AdminMenu_PlayerSetPlayStateConfirm); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Set Play State Confirm", param1, sName); - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 In", param1); - AddMenuItem(hMenu, sUserId, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Out", param1); - AddMenuItem(hMenu, sUserId, sBuffer); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); - } - } -} - -public AdminMenu_PlayerSetPlayStateConfirm(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayPlayerSetPlayStateAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sUserId[64]; - GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); - new client = GetClientOfUserId(StringToInt(sUserId)); - if (client <= 0) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); - } - else - { - new iUserId = StringToInt(sUserId); - switch (param2) - { - case 0: FakeClientCommand(param1, "sm_sf2_setplaystate #%d 1", iUserId); - case 1: FakeClientCommand(param1, "sm_sf2_setplaystate #%d 0", iUserId); - } - } - - DisplayPlayerSetPlayStateAdminMenu(param1); - } -} - -static DisplayBossMainAdminMenu(client) -{ - new Handle:hMenu = CreateMenu(AdminMenu_BossMain); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Main", client); - - decl String:sBuffer[512]; - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Add Boss", client); - AddMenuItem(hMenu, "add_boss", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Add Fake Boss", client); - AddMenuItem(hMenu, "add_boss_fake", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Remove Boss", client); - AddMenuItem(hMenu, "remove_boss", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Spawn Boss", client); - AddMenuItem(hMenu, "spawn_boss", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Boss Attack Waiters", client); - AddMenuItem(hMenu, "boss_attack_waiters", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Boss Teleport", client); - AddMenuItem(hMenu, "boss_no_teleport", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Override Boss", client); - AddMenuItem(hMenu, "override_boss", sBuffer); - - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public AdminMenu_BossMain(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) - { - DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); - } - } - else if (action == MenuAction_Select) - { - decl String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - if (StrEqual(sInfo, "add_boss")) - { - DisplayAddBossAdminMenu(param1); - } - else if (StrEqual(sInfo, "add_boss_fake")) - { - DisplayAddFakeBossAdminMenu(param1); - } - else if (StrEqual(sInfo, "remove_boss")) - { - DisplayRemoveBossAdminMenu(param1); - } - else if (StrEqual(sInfo, "spawn_boss")) - { - DisplaySpawnBossAdminMenu(param1); - } - else if (StrEqual(sInfo, "boss_attack_waiters")) - { - DisplayBossAttackWaitersAdminMenu(param1); - } - else if (StrEqual(sInfo, "boss_no_teleport")) - { - DisplayBossTeleportAdminMenu(param1); - } - else if (StrEqual(sInfo, "override_boss")) - { - DisplayOverrideBossAdminMenu(param1); - } - } -} - -public AdminTopMenu_BossMain(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) -{ - if (action == TopMenuAction_DisplayOption) - { - Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Boss Main", param); - } - else if (action == TopMenuAction_SelectOption) - { - DisplayBossMainAdminMenu(param); - } -} - -static bool:DisplayAddBossAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_AddBoss); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Add Boss", client); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; - - do - { - KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); - KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); - if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); - AddMenuItem(hMenu, sProfile, sDisplayName); - } - while (KvGotoNextKey(g_hConfig)); - - SetMenuExitBackButton(hMenu, true); - - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - - return true; - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_AddBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); - - FakeClientCommand(param1, "sm_sf2_add_boss %s", sProfile); - - DisplayAddBossAdminMenu(param1); - } -} - -static bool:DisplayAddFakeBossAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_AddFakeBoss); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Add Fake Boss", client); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; - - do - { - KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); - KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); - if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); - AddMenuItem(hMenu, sProfile, sDisplayName); - } - while (KvGotoNextKey(g_hConfig)); - - SetMenuExitBackButton(hMenu, true); - - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - - return true; - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_AddFakeBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); - - FakeClientCommand(param1, "sm_sf2_add_boss_fake %s", sProfile); - - DisplayAddFakeBossAdminMenu(param1); - } -} - -static AddBossTargetsToMenu(Handle:hMenu) -{ - if (g_hConfig == INVALID_HANDLE) return 0; - - KvRewind(g_hConfig); - if (!KvGotoFirstSubKey(g_hConfig)) return 0; - - decl String:sBuffer[512]; - decl String:sDisplay[512], String:sInfo[64]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - new iCount; - - for (new i = 0; i < MAX_BOSSES; i++) - { - new iUniqueID = NPCGetUniqueID(i); - if (iUniqueID == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetProfileString(sProfile, "name", sBuffer, sizeof(sBuffer)); - if (strlen(sBuffer) == 0) strcopy(sBuffer, sizeof(sBuffer), sProfile); - - Format(sDisplay, sizeof(sDisplay), "%d - %s", i, sBuffer); - if (g_iSlenderCopyMaster[i] != -1) - { - Format(sBuffer, sizeof(sBuffer), " (copy of boss %d)", g_iSlenderCopyMaster[i]); - StrCat(sDisplay, sizeof(sDisplay), sBuffer); - } - - if (NPCGetFlags(i) & SFF_FAKE) - { - StrCat(sDisplay, sizeof(sDisplay), " (fake)"); - } - - IntToString(iUniqueID, sInfo, sizeof(sInfo)); - - AddMenuItem(hMenu, sInfo, sDisplay); - iCount++; - } - - return iCount; -} - -static bool:DisplayRemoveBossAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_RemoveBoss); - if (!AddBossTargetsToMenu(hMenu)) - { - CloseHandle(hMenu); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); - } - else - { - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Remove Boss", client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - return true; - } - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_RemoveBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - } - else - { - FakeClientCommand(param1, "sm_sf2_remove_boss %d", iIndex); - } - - DisplayRemoveBossAdminMenu(param1); - } -} - -static bool:DisplaySpawnBossAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_SpawnBoss); - if (!AddBossTargetsToMenu(hMenu)) - { - CloseHandle(hMenu); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); - } - else - { - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Spawn Boss", client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - return true; - } - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_SpawnBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - } - else - { - FakeClientCommand(param1, "sm_sf2_spawn_boss %d", iIndex); - } - - DisplaySpawnBossAdminMenu(param1); - } -} - -static bool:DisplayBossAttackWaitersAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_BossAttackWaiters); - if (!AddBossTargetsToMenu(hMenu)) - { - CloseHandle(hMenu); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); - } - else - { - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Attack Waiters", client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - return true; - } - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_BossAttackWaiters(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - DisplayBossAttackWaitersAdminMenu(param1); - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); - - decl String:sName[SF2_MAX_NAME_LENGTH]; - GetProfileString(sProfile, "name", sName, sizeof(sName)); - if (!sName[0]) strcopy(sName, sizeof(sName), sProfile); - - new Handle:hMenu = CreateMenu(AdminMenu_BossAttackWaitersConfirm); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Attack Waiters Confirm", param1, sName); - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); - AddMenuItem(hMenu, sID, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); - AddMenuItem(hMenu, sID, sBuffer); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); - } - } -} - -public AdminMenu_BossAttackWaitersConfirm(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossAttackWaitersAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - } - else - { - switch (param2) - { - case 0: FakeClientCommand(param1, "sm_sf2_boss_attack_waiters %d 1", iIndex); - case 1: FakeClientCommand(param1, "sm_sf2_boss_attack_waiters %d 0", iIndex); - } - } - - DisplayBossAttackWaitersAdminMenu(param1); - } -} - -static bool:DisplayBossTeleportAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_BossTeleport); - if (!AddBossTargetsToMenu(hMenu)) - { - CloseHandle(hMenu); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); - } - else - { - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Teleport", client); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - return true; - } - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_BossTeleport(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - DisplayBossTeleportAdminMenu(param1); - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); - - decl String:sName[SF2_MAX_NAME_LENGTH]; - GetProfileString(sProfile, "name", sName, sizeof(sName)); - if (!sName[0]) strcopy(sName, sizeof(sName), sProfile); - - new Handle:hMenu = CreateMenu(AdminMenu_BossTeleportConfirm); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Teleport Confirm", param1, sName); - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); - AddMenuItem(hMenu, sID, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); - AddMenuItem(hMenu, sID, sBuffer); - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); - } - } -} - -public AdminMenu_BossTeleportConfirm(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossTeleportAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sID[64]; - GetMenuItem(menu, param2, sID, sizeof(sID)); - new iIndex = NPCGetFromUniqueID(StringToInt(sID)); - if (iIndex == -1) - { - CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); - } - else - { - switch (param2) - { - case 0: FakeClientCommand(param1, "sm_sf2_boss_no_teleport %d 0", iIndex); - case 1: FakeClientCommand(param1, "sm_sf2_boss_no_teleport %d 1", iIndex); - } - } - - DisplayBossTeleportAdminMenu(param1); - } -} - -static bool:DisplayOverrideBossAdminMenu(client) -{ - if (g_hConfig != INVALID_HANDLE) - { - KvRewind(g_hConfig); - if (KvGotoFirstSubKey(g_hConfig)) - { - new Handle:hMenu = CreateMenu(AdminMenu_OverrideBoss); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; - - do - { - KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); - KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); - if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); - AddMenuItem(hMenu, sProfile, sDisplayName); - } - while (KvGotoNextKey(g_hConfig)); - - SetMenuExitBackButton(hMenu, true); - - new String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH], String:sProfileDisplayName[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); - - if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) - { - GetProfileString(sProfileOverride, "name", sProfileDisplayName, sizeof(sProfileDisplayName)); - - if (strlen(sProfileDisplayName) == 0) - strcopy(sProfileDisplayName, sizeof(sProfileDisplayName), sProfileOverride) - } - else - strcopy(sProfileDisplayName, sizeof(sProfileDisplayName), "---"); - - SetMenuTitle(hMenu, "%t%T\n%T\n \n", "SF2 Prefix", "SF2 Admin Menu Override Boss", client, "SF2 Admin Menu Current Boss Override", client, sProfileDisplayName); - - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - - return true; - } - } - - DisplayBossMainAdminMenu(client); - return false; -} - -public AdminMenu_OverrideBoss(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayBossMainAdminMenu(param1); - } - } - else if (action == MenuAction_Select) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); - - FakeClientCommand(param1, "sm_cvar sf2_boss_profile_override %s", sProfile); - - DisplayOverrideBossAdminMenu(param1); - } +#if defined _sf2_adminmenu_included + #endinput +#endif +#define _sf2_adminmenu_included + +static Handle:g_hTopMenu = INVALID_HANDLE; +static g_iPlayerAdminMenuTargetUserId[MAXPLAYERS + 1] = { -1, ... }; + +SetupAdminMenu() +{ + /* Account for late loading */ + new Handle:hTopMenu = INVALID_HANDLE; + if (LibraryExists("adminmenu") && ((hTopMenu = GetAdminTopMenu()) != INVALID_HANDLE)) + { + OnAdminMenuReady(hTopMenu); + } +} + + +public OnAdminMenuReady(Handle:hTopMenu) +{ + if (hTopMenu == g_hTopMenu) return; + + g_hTopMenu = hTopMenu; + + new TopMenuObject:hServerCommands = FindTopMenuCategory(hTopMenu, ADMINMENU_SERVERCOMMANDS); + if (hServerCommands != INVALID_TOPMENUOBJECT) + { + AddToTopMenu(hTopMenu, "sf2_boss_admin_main", TopMenuObject_Item, AdminTopMenu_BossMain, hServerCommands, "sm_sf2_add_boss", ADMFLAG_SLAY); + } + + new TopMenuObject:hPlayerCommands = FindTopMenuCategory(hTopMenu, ADMINMENU_PLAYERCOMMANDS); + if (hPlayerCommands != INVALID_TOPMENUOBJECT) + { + AddToTopMenu(hTopMenu, "sf2_player_setplaystate", TopMenuObject_Item, AdminTopMenu_PlayerSetPlayState, hPlayerCommands, "sm_sf2_setplaystate", ADMFLAG_SLAY); + AddToTopMenu(hTopMenu, "sf2_player_force_proxy", TopMenuObject_Item, AdminTopMenu_PlayerForceProxy, hPlayerCommands, "sm_sf2_force_proxy", ADMFLAG_SLAY); + } +} + +static DisplayPlayerForceProxyAdminMenu(client) +{ + new Handle:hMenu = CreateMenu(AdminMenu_PlayerForceProxy); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy", client); + AddTargetsToMenu(hMenu, client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public AdminTopMenu_PlayerForceProxy(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) +{ + if (action == TopMenuAction_DisplayOption) + { + Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy", param); + } + else if (action == TopMenuAction_SelectOption) + { + DisplayPlayerForceProxyAdminMenu(param); + } +} + +public AdminMenu_PlayerForceProxy(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) + { + DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); + } + } + else if (action == MenuAction_Select) + { + decl String:sUserId[64]; + GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); + + new client = GetClientOfUserId(StringToInt(sUserId)); + if (client <= 0) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); + DisplayPlayerForceProxyAdminMenu(param1); + } + else + { + g_iPlayerAdminMenuTargetUserId[param1] = StringToInt(sUserId); + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + new Handle:hMenu = CreateMenu(AdminMenu_PlayerForceProxyBoss); + if (!AddBossTargetsToMenu(hMenu)) + { + CloseHandle(hMenu); + DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", param1); + } + else + { + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Force Proxy Boss", param1, sName); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); + } + } + } +} + +public AdminMenu_PlayerForceProxyBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayPlayerForceProxyAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + new client = GetClientOfUserId(g_iPlayerAdminMenuTargetUserId[param1]); + if (client <= 0) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); + } + else + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); + + if (!bool:GetProfileNum(sProfile, "proxies", 0) || + g_iSlenderCopyMaster[iIndex] != -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Not Allowed To Have Proxies", param1); + } + else if (!g_bPlayerEliminated[client]) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player In Game", param1); + } + else if (g_bPlayerProxy[param1]) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Already A Proxy", param1); + } + else + { + FakeClientCommand(param1, "sm_sf2_force_proxy #%d %d", g_iPlayerAdminMenuTargetUserId[param1], iIndex); + } + } + } + + DisplayPlayerForceProxyAdminMenu(param1); + } +} + +static DisplayPlayerSetPlayStateAdminMenu(client) +{ + new Handle:hMenu = CreateMenu(AdminMenu_PlayerSetPlayState); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Set Play State", client); + AddTargetsToMenu(hMenu, client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public AdminTopMenu_PlayerSetPlayState(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) +{ + if (action == TopMenuAction_DisplayOption) + { + Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Player Set Play State", param); + } + else if (action == TopMenuAction_SelectOption) + { + DisplayPlayerSetPlayStateAdminMenu(param); + } +} + +public AdminMenu_PlayerSetPlayState(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) + { + DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); + } + } + else if (action == MenuAction_Select) + { + decl String:sUserId[64]; + GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); + new client = GetClientOfUserId(StringToInt(sUserId)); + if (client <= 0) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); + DisplayPlayerSetPlayStateAdminMenu(param1); + } + else + { + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + new Handle:hMenu = CreateMenu(AdminMenu_PlayerSetPlayStateConfirm); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Player Set Play State Confirm", param1, sName); + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 In", param1); + AddMenuItem(hMenu, sUserId, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Out", param1); + AddMenuItem(hMenu, sUserId, sBuffer); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); + } + } +} + +public AdminMenu_PlayerSetPlayStateConfirm(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayPlayerSetPlayStateAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sUserId[64]; + GetMenuItem(menu, param2, sUserId, sizeof(sUserId)); + new client = GetClientOfUserId(StringToInt(sUserId)); + if (client <= 0) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Player Does Not Exist", param1); + } + else + { + new iUserId = StringToInt(sUserId); + switch (param2) + { + case 0: FakeClientCommand(param1, "sm_sf2_setplaystate #%d 1", iUserId); + case 1: FakeClientCommand(param1, "sm_sf2_setplaystate #%d 0", iUserId); + } + } + + DisplayPlayerSetPlayStateAdminMenu(param1); + } +} + +static DisplayBossMainAdminMenu(client) +{ + new Handle:hMenu = CreateMenu(AdminMenu_BossMain); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Main", client); + + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Add Boss", client); + AddMenuItem(hMenu, "add_boss", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Add Fake Boss", client); + AddMenuItem(hMenu, "add_boss_fake", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Remove Boss", client); + AddMenuItem(hMenu, "remove_boss", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Spawn Boss", client); + AddMenuItem(hMenu, "spawn_boss", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Boss Attack Waiters", client); + AddMenuItem(hMenu, "boss_attack_waiters", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Boss Teleport", client); + AddMenuItem(hMenu, "boss_no_teleport", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Menu Override Boss", client); + AddMenuItem(hMenu, "override_boss", sBuffer); + + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public AdminMenu_BossMain(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack && g_hTopMenu != INVALID_HANDLE) + { + DisplayTopMenu(g_hTopMenu, param1, TopMenuPosition_LastCategory); + } + } + else if (action == MenuAction_Select) + { + decl String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + if (StrEqual(sInfo, "add_boss")) + { + DisplayAddBossAdminMenu(param1); + } + else if (StrEqual(sInfo, "add_boss_fake")) + { + DisplayAddFakeBossAdminMenu(param1); + } + else if (StrEqual(sInfo, "remove_boss")) + { + DisplayRemoveBossAdminMenu(param1); + } + else if (StrEqual(sInfo, "spawn_boss")) + { + DisplaySpawnBossAdminMenu(param1); + } + else if (StrEqual(sInfo, "boss_attack_waiters")) + { + DisplayBossAttackWaitersAdminMenu(param1); + } + else if (StrEqual(sInfo, "boss_no_teleport")) + { + DisplayBossTeleportAdminMenu(param1); + } + else if (StrEqual(sInfo, "override_boss")) + { + DisplayOverrideBossAdminMenu(param1); + } + } +} + +public AdminTopMenu_BossMain(Handle:topmenu, TopMenuAction:action, TopMenuObject:object_id, param, String:buffer[], maxlength) +{ + if (action == TopMenuAction_DisplayOption) + { + Format(buffer, maxlength, "%t%T", "SF2 Prefix", "SF2 Admin Menu Boss Main", param); + } + else if (action == TopMenuAction_SelectOption) + { + DisplayBossMainAdminMenu(param); + } +} + +static bool:DisplayAddBossAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_AddBoss); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Add Boss", client); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; + + do + { + KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); + KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); + if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); + AddMenuItem(hMenu, sProfile, sDisplayName); + } + while (KvGotoNextKey(g_hConfig)); + + SetMenuExitBackButton(hMenu, true); + + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + + return true; + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_AddBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); + + FakeClientCommand(param1, "sm_sf2_add_boss %s", sProfile); + + DisplayAddBossAdminMenu(param1); + } +} + +static bool:DisplayAddFakeBossAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_AddFakeBoss); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Add Fake Boss", client); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; + + do + { + KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); + KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); + if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); + AddMenuItem(hMenu, sProfile, sDisplayName); + } + while (KvGotoNextKey(g_hConfig)); + + SetMenuExitBackButton(hMenu, true); + + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + + return true; + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_AddFakeBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); + + FakeClientCommand(param1, "sm_sf2_add_boss_fake %s", sProfile); + + DisplayAddFakeBossAdminMenu(param1); + } +} + +static AddBossTargetsToMenu(Handle:hMenu) +{ + if (g_hConfig == INVALID_HANDLE) return 0; + + KvRewind(g_hConfig); + if (!KvGotoFirstSubKey(g_hConfig)) return 0; + + decl String:sBuffer[512]; + decl String:sDisplay[512], String:sInfo[64]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + new iCount; + + for (new i = 0; i < MAX_BOSSES; i++) + { + new iUniqueID = NPCGetUniqueID(i); + if (iUniqueID == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetProfileString(sProfile, "name", sBuffer, sizeof(sBuffer)); + if (strlen(sBuffer) == 0) strcopy(sBuffer, sizeof(sBuffer), sProfile); + + Format(sDisplay, sizeof(sDisplay), "%d - %s", i, sBuffer); + if (g_iSlenderCopyMaster[i] != -1) + { + Format(sBuffer, sizeof(sBuffer), " (copy of boss %d)", g_iSlenderCopyMaster[i]); + StrCat(sDisplay, sizeof(sDisplay), sBuffer); + } + + if (NPCGetFlags(i) & SFF_FAKE) + { + StrCat(sDisplay, sizeof(sDisplay), " (fake)"); + } + + IntToString(iUniqueID, sInfo, sizeof(sInfo)); + + AddMenuItem(hMenu, sInfo, sDisplay); + iCount++; + } + + return iCount; +} + +static bool:DisplayRemoveBossAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_RemoveBoss); + if (!AddBossTargetsToMenu(hMenu)) + { + CloseHandle(hMenu); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); + } + else + { + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Remove Boss", client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + return true; + } + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_RemoveBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + } + else + { + FakeClientCommand(param1, "sm_sf2_remove_boss %d", iIndex); + } + + DisplayRemoveBossAdminMenu(param1); + } +} + +static bool:DisplaySpawnBossAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_SpawnBoss); + if (!AddBossTargetsToMenu(hMenu)) + { + CloseHandle(hMenu); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); + } + else + { + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Spawn Boss", client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + return true; + } + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_SpawnBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + } + else + { + FakeClientCommand(param1, "sm_sf2_spawn_boss %d", iIndex); + } + + DisplaySpawnBossAdminMenu(param1); + } +} + +static bool:DisplayBossAttackWaitersAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_BossAttackWaiters); + if (!AddBossTargetsToMenu(hMenu)) + { + CloseHandle(hMenu); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); + } + else + { + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Attack Waiters", client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + return true; + } + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_BossAttackWaiters(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + DisplayBossAttackWaitersAdminMenu(param1); + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); + + decl String:sName[SF2_MAX_NAME_LENGTH]; + GetProfileString(sProfile, "name", sName, sizeof(sName)); + if (!sName[0]) strcopy(sName, sizeof(sName), sProfile); + + new Handle:hMenu = CreateMenu(AdminMenu_BossAttackWaitersConfirm); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Attack Waiters Confirm", param1, sName); + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + AddMenuItem(hMenu, sID, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + AddMenuItem(hMenu, sID, sBuffer); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); + } + } +} + +public AdminMenu_BossAttackWaitersConfirm(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossAttackWaitersAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + } + else + { + switch (param2) + { + case 0: FakeClientCommand(param1, "sm_sf2_boss_attack_waiters %d 1", iIndex); + case 1: FakeClientCommand(param1, "sm_sf2_boss_attack_waiters %d 0", iIndex); + } + } + + DisplayBossAttackWaitersAdminMenu(param1); + } +} + +static bool:DisplayBossTeleportAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_BossTeleport); + if (!AddBossTargetsToMenu(hMenu)) + { + CloseHandle(hMenu); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 No Active Bosses", client); + } + else + { + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Teleport", client); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + return true; + } + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_BossTeleport(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + DisplayBossTeleportAdminMenu(param1); + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iIndex, sProfile, sizeof(sProfile)); + + decl String:sName[SF2_MAX_NAME_LENGTH]; + GetProfileString(sProfile, "name", sName, sizeof(sName)); + if (!sName[0]) strcopy(sName, sizeof(sName), sProfile); + + new Handle:hMenu = CreateMenu(AdminMenu_BossTeleportConfirm); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Admin Menu Boss Teleport Confirm", param1, sName); + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + AddMenuItem(hMenu, sID, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + AddMenuItem(hMenu, sID, sBuffer); + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, param1, MENU_TIME_FOREVER); + } + } +} + +public AdminMenu_BossTeleportConfirm(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossTeleportAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sID[64]; + GetMenuItem(menu, param2, sID, sizeof(sID)); + new iIndex = NPCGetFromUniqueID(StringToInt(sID)); + if (iIndex == -1) + { + CPrintToChat(param1, "%t%T", "SF2 Prefix", "SF2 Boss Does Not Exist", param1); + } + else + { + switch (param2) + { + case 0: FakeClientCommand(param1, "sm_sf2_boss_no_teleport %d 0", iIndex); + case 1: FakeClientCommand(param1, "sm_sf2_boss_no_teleport %d 1", iIndex); + } + } + + DisplayBossTeleportAdminMenu(param1); + } +} + +static bool:DisplayOverrideBossAdminMenu(client) +{ + if (g_hConfig != INVALID_HANDLE) + { + KvRewind(g_hConfig); + if (KvGotoFirstSubKey(g_hConfig)) + { + new Handle:hMenu = CreateMenu(AdminMenu_OverrideBoss); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sDisplayName[SF2_MAX_NAME_LENGTH]; + + do + { + KvGetSectionName(g_hConfig, sProfile, sizeof(sProfile)); + KvGetString(g_hConfig, "name", sDisplayName, sizeof(sDisplayName)); + if (!sDisplayName[0]) strcopy(sDisplayName, sizeof(sDisplayName), sProfile); + AddMenuItem(hMenu, sProfile, sDisplayName); + } + while (KvGotoNextKey(g_hConfig)); + + SetMenuExitBackButton(hMenu, true); + + new String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH], String:sProfileDisplayName[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); + + if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) + { + GetProfileString(sProfileOverride, "name", sProfileDisplayName, sizeof(sProfileDisplayName)); + + if (strlen(sProfileDisplayName) == 0) + strcopy(sProfileDisplayName, sizeof(sProfileDisplayName), sProfileOverride) + } + else + strcopy(sProfileDisplayName, sizeof(sProfileDisplayName), "---"); + + SetMenuTitle(hMenu, "%t%T\n%T\n \n", "SF2 Prefix", "SF2 Admin Menu Override Boss", client, "SF2 Admin Menu Current Boss Override", client, sProfileDisplayName); + + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + + return true; + } + } + + DisplayBossMainAdminMenu(client); + return false; +} + +public AdminMenu_OverrideBoss(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayBossMainAdminMenu(param1); + } + } + else if (action == MenuAction_Select) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetMenuItem(menu, param2, sProfile, sizeof(sProfile)); + + FakeClientCommand(param1, "sm_cvar sf2_boss_profile_override %s", sProfile); + + DisplayOverrideBossAdminMenu(param1); + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/client.sp b/addons/sourcemod/scripting/rytp_horror/client.sp index 9e06990..f3626f5 100644 --- a/addons/sourcemod/scripting/rytp_horror/client.sp +++ b/addons/sourcemod/scripting/rytp_horror/client.sp @@ -1,5966 +1,5888 @@ -#if defined _sf2_client_included - #endinput -#endif -#define _sf2_client_included - -#define GHOST_MODEL "models/props_halloween/ghost_no_hat.mdl" -//#define BLACK_OVERLAY "overlays/slender/newcamerahud" - -#define EF_NODRAW 32 - -#define SF2_FLASHLIGHT_WIDTH 512.0 // How wide the player's Flashlight should be in world units. -#define SF2_FLASHLIGHT_LENGTH 1024.0 // How far the player's Flashlight can reach in world units. -#define SF2_FLASHLIGHT_BRIGHTNESS 0 // Intensity of the players' Flashlight. -#define SF2_FLASHLIGHT_DRAIN_RATE 0.65 // How long (in seconds) each bar on the player's Flashlight meter lasts. -#define SF2_FLASHLIGHT_RECHARGE_RATE 0.68 // How long (in seconds) it takes each bar on the player's Flashlight meter to recharge. -#define SF2_FLASHLIGHT_FLICKERAT 0.25 // The percentage of the Flashlight battery where the Flashlight will start to blink. -#define SF2_FLASHLIGHT_ENABLEAT 0.3 // The percentage of the Flashlight battery where the Flashlight will be able to be used again (if the player shortens out the Flashlight from excessive use). -#define SF2_FLASHLIGHT_COOLDOWN 0.4 // How much time players have to wait before being able to switch their flashlight on again after turning it off. - -#define SF2_ULTRAVISION_WIDTH 800.0 -#define SF2_ULTRAVISION_LENGTH 800.0 -#define SF2_ULTRAVISION_BRIGHTNESS -4 // Intensity of Ultravision. -#define SF2_ULTRAVISION_CONE 180.0 - -#define SF2_PLAYER_BREATH_COOLDOWN_MIN 0.8 -#define SF2_PLAYER_BREATH_COOLDOWN_MAX 2.0 - -#define SF2_BREATH_VIEWBOB_SPEED 0.05 -#define SF2_BREATH_VIEWBOB_START 0.045 -#define SF2_BREATH_VIEWBOB_AMPLITUDE 0.17 - -new String:g_strPlayerBreathSounds[][] = -{ - "rytp_horror/player_breath_1.wav" -}; - -new String:g_strGhostHelpPhrases[][] = -{ - "rytp_horror/ghost/epifancev_zdes.mp3", - "rytp_horror/ghost/pahom_ohh.mp3", - "rytp_horror/ghost/pahom_ja_nichego_net_net.mp3", - "rytp_horror/ghost/zs_epifan_vou.mp3", - "rytp_horror/ghost/zs_mmm.mp3", - "rytp_horror/ghost/zs_mmm2.mp3" -}; -new g_iGhostHelpPhraseInterval = 2; -new g_iGhostNextHelpPhrase[MAXPLAYERS + 1]; - -static String:g_strPlayerLagCompensationWeapons[][] = -{ - "tf_weapon_sniperrifle", - "tf_weapon_sniperrifle_decap", - "tf_weapon_sniperrifle_classic" -}; - -// Deathcam data. -static g_iPlayerDeathCamBoss[MAXPLAYERS + 1] = { -1, ... }; -static bool:g_bPlayerDeathCam[MAXPLAYERS + 1] = { false, ... }; -static bool:g_bPlayerDeathCamShowOverlay[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerDeathCamEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerDeathCamEnt2[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Handle:g_hPlayerDeathCamTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Flashlight data. -static bool:g_bPlayerFlashlight[MAXPLAYERS + 1] = { false, ... }; -static bool:g_bPlayerFlashlightBroken[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerFlashlightEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerFlashlightEntAng[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Float:g_flPlayerFlashlightBatteryLife[MAXPLAYERS + 1] = { 1.0, ... }; -static Handle:g_hPlayerFlashlightBatteryTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerFlashlightNextInputTime[MAXPLAYERS + 1] = { -1.0, ... }; - -// Ultravision data. -static bool:g_bPlayerUltravision[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerUltravisionEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; - -// Sprint data. -static bool:g_bPlayerSprint[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerSprintPoints[MAXPLAYERS + 1] = { 100, ... }; -static Handle:g_hPlayerSprintTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Blink data. -static Handle:g_hPlayerBlinkTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static bool:g_bPlayerBlink[MAXPLAYERS + 1] = { false, ... }; -static Float:g_flPlayerBlinkMeter[MAXPLAYERS + 1] = { 0.0, ... }; -static g_iPlayerBlinkCount[MAXPLAYERS + 1] = { 0, ... }; - -// Breathing data. -static bool:g_bPlayerBreath[MAXPLAYERS + 1] = { false, ... }; -static Handle:g_hPlayerBreathTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -new Float:g_flPlayerBreathViewbobPhase[MAXPLAYERS + 1]; -new Float:g_flPlayerBreathViewbobXMult[MAXPLAYERS + 1] = 1.0; -new Float:g_flPlayerBreathViewbobYMult[MAXPLAYERS + 1] = 1.0; - -// Interactive glow data. -static g_iPlayerInteractiveGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerInteractiveGlowTargetEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; - -// Constant glow data. -static g_iPlayerConstantGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static bool:g_bPlayerConstantGlowEnabled[MAXPLAYERS + 1] = { false, ... }; - -// Jumpscare data. -static g_iPlayerJumpScareBoss[MAXPLAYERS + 1] = { -1, ... }; -static Float:g_flPlayerJumpScareLifeTime[MAXPLAYERS + 1] = { -1.0, ... }; - -static Float:g_flPlayerScareBoostEndTime[MAXPLAYERS + 1] = { -1.0, ... }; - -// Anti-camping data. -static g_iPlayerCampingStrikes[MAXPLAYERS + 1] = { 0, ... }; -static Handle:g_hPlayerCampingTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerCampingLastPosition[MAXPLAYERS + 1][3]; -static bool:g_bPlayerCampingFirstTime[MAXPLAYERS + 1] = { true, ... }; - - -// ========================================================== -// GENERAL CLIENT HOOK FUNCTIONS -// ========================================================== - -#define SF2_PLAYER_VIEWBOB_TIMER 10.0 -#define SF2_PLAYER_VIEWBOB_SCALE_X 0.05 -#define SF2_PLAYER_VIEWBOB_SCALE_Y 0.0 -#define SF2_PLAYER_VIEWBOB_SCALE_Z 0.0 - - -public MRESReturn:Hook_ClientWantsLagCompensationOnEntity(thisPointer, Handle:hReturn, Handle:hParams) -{ - if (!g_bEnabled || IsFakeClient(thisPointer)) return MRES_Ignored; - - DHookSetReturn(hReturn, true); - return MRES_Supercede; -} - -Float:ClientGetScareBoostEndTime(client) -{ - return g_flPlayerScareBoostEndTime[client]; -} - -ClientSetScareBoostEndTime(client, Float:time) -{ - g_flPlayerScareBoostEndTime[client] = time; -} - -public Hook_ClientPreThink(client) -{ - if (!g_bEnabled) return; - - ClientProcessViewAngles(client); - ClientProcessVisibility(client); - ClientProcessStaticShake(client); - ClientProcessFlashlightAngles(client); - ClientProcessInteractiveGlow(client); - - if (IsClientInGhostMode(client)) - { - SetEntPropFloat(client, Prop_Send, "m_flNextAttack", GetGameTime() + 2.0); - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 520.0); - } - else if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) - { - if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) - { - new iRoundState = _:GameRules_GetRoundState(); - - Hide_Weapon(client); - - // No double jumping for players in play. - SetEntProp(client, Prop_Send, "m_iAirDash", 99999); - - if (!g_bPlayerProxy[client]) - { - if (iRoundState == 4) - { - new bool:bDanger = false; - - if (!bDanger) - { - decl iState; - decl iBossTarget; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetType(i) == SF2BossType_Chaser) - { - iBossTarget = EntRefToEntIndex(g_iSlenderTarget[i]); - iState = g_iSlenderState[i]; - - if ((iState == STATE_CHASE || iState == STATE_ATTACK || iState == STATE_STUN) && - ((iBossTarget && iBossTarget != INVALID_ENT_REFERENCE && (iBossTarget == client || ClientGetDistanceFromEntity(client, iBossTarget) < 512.0)) || NPCGetDistanceFromEntity(i, client) < 512.0 || PlayerCanSeeSlender(client, i, false))) - { - bDanger = true; - ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); - - // Induce client stress levels. - new Float:flUnComfortZoneDist = 512.0; - new Float:flStressScalar = (flUnComfortZoneDist / NPCGetDistanceFromEntity(i, client)); - ClientAddStress(client, 0.025 * flStressScalar); - - break; - } - } - } - } - - if (g_flPlayerStaticAmount[client] > 0.4) bDanger = true; - if (GetGameTime() < ClientGetScareBoostEndTime(client)) bDanger = true; - - if (!bDanger) - { - decl iState; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetType(i) == SF2BossType_Chaser) - { - if (iState == STATE_ALERT) - { - if (PlayerCanSeeSlender(client, i)) - { - bDanger = true; - ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); - } - } - } - } - } - - if (!bDanger) - { - new Float:flCurTime = GetGameTime(); - new Float:flScareSprintDuration = 3.0; - if (TF2_GetPlayerClass(client) == TFClass_DemoMan) flScareSprintDuration *= 1.667; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if ((flCurTime - g_flPlayerScareLastTime[client][i]) <= flScareSprintDuration) - { - bDanger = true; - break; - } - } - } - - new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); - new Float:flSprintSpeed = ClientGetDefaultSprintSpeed(client); - - // Check for weapon speed changes. - new iWeapon = INVALID_ENT_REFERENCE; - - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; - - new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - switch (iItemDef) - { - case 239: // Gloves of Running Urgently - { - if (GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon") == iWeapon) - { - flSprintSpeed += (flSprintSpeed * 0.1); - } - } - case 775: // Escape Plan - { - new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); - new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); - new Float:flPercentage = flHealth / flMaxHealth; - - if (flPercentage < 0.805 && flPercentage >= 0.605) flSprintSpeed += (flSprintSpeed * 0.05); - else if (flPercentage < 0.605 && flPercentage >= 0.405) flSprintSpeed += (flSprintSpeed * 0.1); - else if (flPercentage < 0.405 && flPercentage >= 0.205) flSprintSpeed += (flSprintSpeed * 0.15); - else if (flPercentage < 0.205) flSprintSpeed += (flSprintSpeed * 0.2); - } - } - } - - // Speed buff? - if (TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly)) - { - flWalkSpeed += (flWalkSpeed * 0.08); - flSprintSpeed += (flSprintSpeed * 0.08); - } - - if (bDanger) - { - flWalkSpeed *= 1.33; - flSprintSpeed *= 1.33; - - if (!g_bPlayerHints[client][PlayerHint_Sprint]) - { - ClientShowHint(client, PlayerHint_Sprint); - } - } - - new Float:flSprintSpeedSubtract = ((flSprintSpeed - flWalkSpeed) * 0.5); - flSprintSpeedSubtract -= flSprintSpeedSubtract * (g_iPlayerSprintPoints[client] != 0 ? (float(g_iPlayerSprintPoints[client]) / 100.0) : 0.0); - flSprintSpeed -= flSprintSpeedSubtract; - - if (IsClientSprinting(client)) - { - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flSprintSpeed); - } - else - { - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flWalkSpeed); - } - - if (ClientCanBreath(client) && !g_bPlayerBreath[client]) - { - ClientStartBreathing(client); - } - } - } - else - { - new TFClassType:iClass = TF2_GetPlayerClass(client); - new bool:bSpeedup = TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly); - - switch (iClass) - { - case TFClass_Scout: - { - if (iRoundState == 4) - { - if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 405.0); - else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); - } - } - case TFClass_Medic: - { - if (iRoundState == 4) - { - if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 385.0); - else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); - } - } - } - } - } - } - - // Calculate player stress levels. - if (GetGameTime() >= g_flPlayerStressNextUpdateTime[client]) - { - //new Float:flPagePercent = g_iPageMax != 0 ? float(g_iPageCount) / float(g_iPageMax) : 0.0; - //new Float:flPageCountPercent = g_iPageMax != 0? float(g_iPlayerPageCount[client]) / float(g_iPageMax) : 0.0; - - g_flPlayerStressNextUpdateTime[client] = GetGameTime() + 0.33; - ClientAddStress(client, -0.01); - -#if defined DEBUG - SendDebugMessageToPlayer(client, DEBUG_PLAYER_STRESS, 1, "g_flPlayerStress[%d]: %0.1f", client, g_flPlayerStress[client]); -#endif - } - - // Process screen shake, if enabled. - if (g_bPlayerShakeEnabled) - { - new bool:bDoShake = false; - - if (IsPlayerAlive(client)) - { - new iStaticMaster = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); - if (iStaticMaster != -1 && NPCGetFlags(iStaticMaster) & SFF_HASVIEWSHAKE) - { - bDoShake = true; - } - } - - if (bDoShake) - { - new Float:flPercent = g_flPlayerStaticAmount[client]; - - new Float:flAmplitudeMax = GetConVarFloat(g_cvPlayerShakeAmplitudeMax); - new Float:flAmplitude = flAmplitudeMax * flPercent; - - new Float:flFrequencyMax = GetConVarFloat(g_cvPlayerShakeFrequencyMax); - new Float:flFrequency = flFrequencyMax * flPercent; - - UTIL_ScreenShake(client, flAmplitude, 0.5, flFrequency); - } - } -} - -public Action:Hook_ClientSetTransmit(client, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (other != client) - { - if (IsClientInGhostMode(client) && !IsClientInGhostMode(other)) return Plugin_Handled; - - if (!IsRoundEnding()) - { - // SPECIAL ROUND: Singleplayer - /* - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (!g_bPlayerEliminated[client] && !g_bPlayerEliminated[other] && !DidClientEscape(other)) return Plugin_Handled; - } - */ - // pvp - if (IsClientInPvP(client) && IsClientInPvP(other)) - { - if (TF2_IsPlayerInCondition(client, TFCond_Cloaked) && - !TF2_IsPlayerInCondition(client, TFCond_CloakFlicker) && - !TF2_IsPlayerInCondition(client, TFCond_Jarated) && - !TF2_IsPlayerInCondition(client, TFCond_Milked) && - !TF2_IsPlayerInCondition(client, TFCond_OnFire) && - (GetGameTime() > GetEntPropFloat(client, Prop_Send, "m_flInvisChangeCompleteTime"))) - { - return Plugin_Handled; - } - } - } - } - - return Plugin_Continue; -} - -public Action:TF2_CalcIsAttackCritical(client, weapon, String:sWeaponName[], &bool:result) -{ - if (!g_bEnabled) return Plugin_Continue; - - if ((IsRoundInWarmup() || IsClientInPvP(client)) && !IsRoundEnding()) - { - if (!GetConVarBool(g_cvPlayerFakeLagCompensation)) - { - new bool:bNeedsManualDamage = false; - - // Fake lag compensation isn't enabled; check to see if we need to deal damage manually. - for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) - { - if (StrEqual(sWeaponName, g_strPlayerLagCompensationWeapons[i], false)) - { - bNeedsManualDamage = true; - break; - } - } - - if (bNeedsManualDamage) - { - decl Float:flStartPos[3], Float:flEyeAng[3]; - GetClientEyePosition(client, flStartPos); - GetClientEyeAngles(client, flEyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flEyeAng, MASK_SHOT, RayType_Infinite, TraceRayDontHitEntity, client); - new iHitEntity = TR_GetEntityIndex(hTrace); - new iHitGroup = TR_GetHitGroup(hTrace); - CloseHandle(hTrace); - - if (IsValidClient(iHitEntity)) - { - if (GetClientTeam(iHitEntity) == GetClientTeam(client)) - { - if (IsRoundInWarmup() || IsClientInPvP(iHitEntity)) - { - new Float:flChargedDamage = GetEntPropFloat(weapon, Prop_Send, "m_flChargedDamage"); - if (flChargedDamage < 50.0) flChargedDamage = 50.0; - new iDamageType = DMG_BULLET; - - if (IsClientCritBoosted(client)) - { - result = true; - iDamageType |= DMG_ACID; - } - else if (iHitGroup == 1) - { - if (StrEqual(sWeaponName, "tf_weapon_sniperrifle_classic", false)) - { - if (flChargedDamage >= 150.0) - { - result = true; - iDamageType |= DMG_ACID; - } - } - else - { - if (TF2_IsPlayerInCondition(client, TFCond_Zoomed)) - { - result = true; - iDamageType |= DMG_ACID; - } - } - } - - SDKHooks_TakeDamage(iHitEntity, client, client, flChargedDamage, iDamageType); - return Plugin_Changed; - } - } - } - } - } - } - - return Plugin_Continue; -} - -public Action:Hook_ClientOnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsRoundInWarmup()) return Plugin_Continue; - - if (attacker != victim && IsValidClient(attacker)) - { - if (!IsRoundEnding()) - { - if (IsClientInPvP(victim) && IsClientInPvP(attacker)) - { - if (attacker == inflictor) - { - if (IsValidEdict(weapon)) - { - decl String:sWeaponClass[64]; - GetEdictClassname(weapon, sWeaponClass, sizeof(sWeaponClass)); - - // Backstab check! - if ((StrEqual(sWeaponClass, "tf_weapon_knife", false) || (TF2_GetPlayerClass(attacker) == TFClass_Spy && StrEqual(sWeaponClass, "saxxy", false))) && - (damagecustom != TF_CUSTOM_TAUNT_FENCING)) - { - decl Float:flMyPos[3], Float:flHisPos[3], Float:flMyDirection[3]; - GetClientAbsOrigin(victim, flMyPos); - GetClientAbsOrigin(attacker, flHisPos); - GetClientEyeAngles(victim, flMyDirection); - GetAngleVectors(flMyDirection, flMyDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flMyDirection, flMyDirection); - ScaleVector(flMyDirection, 32.0); - AddVectors(flMyDirection, flMyPos, flMyDirection); - - decl Float:p[3], Float:s[3]; - MakeVectorFromPoints(flMyPos, flHisPos, p); - MakeVectorFromPoints(flMyPos, flMyDirection, s); - if (GetVectorDotProduct(p, s) <= 0.0) - { - damage = float(GetEntProp(victim, Prop_Send, "m_iHealth")) * 2.0; - - new Handle:hCvar = FindConVar("tf_weapon_criticals"); - if (hCvar != INVALID_HANDLE && GetConVarBool(hCvar)) damagetype |= DMG_ACID; - return Plugin_Changed; - } - } - } - } - } - /* - else if (g_bPlayerProxy[victim] || g_bPlayerProxy[attacker]) - { - if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) - { - damage = 0.0; - return Plugin_Changed; - } - - if (g_bPlayerProxy[attacker]) - { - new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, victim); - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[attacker]); - if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) - { - if (damagecustom == TF_CUSTOM_TAUNT_GRAND_SLAM || - damagecustom == TF_CUSTOM_TAUNT_FENCING || - damagecustom == TF_CUSTOM_TAUNT_ARROW_STAB || - damagecustom == TF_CUSTOM_TAUNT_GRENADE || - damagecustom == TF_CUSTOM_TAUNT_BARBARIAN_SWING || - damagecustom == TF_CUSTOM_TAUNT_ENGINEER_ARM || - damagecustom == TF_CUSTOM_TAUNT_ARMAGEDDON) - { - if (damage >= float(iMaxHealth)) damage = float(iMaxHealth) * 0.5; - else damage = 0.0; - } - else if (damagecustom == TF_CUSTOM_BACKSTAB) // Modify backstab damage. - { - damage = float(iMaxHealth) * GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy_backstab", 0.25); - if (damagetype & DMG_ACID) damage /= 3.0; - } - - g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitenemy"); - if (g_iPlayerProxyControl[attacker] > 100) - { - g_iPlayerProxyControl[attacker] = 100; - } - - damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy", 1.0); - } - - return Plugin_Changed; - } - else if (g_bPlayerProxy[victim]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[victim]); - if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) - { - g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitbyenemy"); - if (g_iPlayerProxyControl[attacker] > 100) - { - g_iPlayerProxyControl[attacker] = 100; - } - - damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_self", 1.0); - } - - return Plugin_Changed; - } - } - */ - else - { - damage = 0.0; - return Plugin_Changed; - } - } - else - { - if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) - { - damage = 0.0; - return Plugin_Changed; - } - } - - if (IsClientInGhostMode(victim)) - { - damage = 0.0; - return Plugin_Changed; - } - } - - return Plugin_Continue; -} - -public Action:Hook_TEFireBullets(const String:te_name[], const Players[], numClients, Float:delay) -{ - if (!g_bEnabled) return Plugin_Continue; - - new client = TE_ReadNum("m_iPlayer") + 1; - if (IsValidClient(client)) - { - if (GetConVarBool(g_cvPlayerFakeLagCompensation)) - { - if ((IsRoundInWarmup() || IsClientInPvP(client))) - { - ClientEnableFakeLagCompensation(client); - } - } - } - - return Plugin_Continue; -} - -ClientResetStatic(client) -{ - g_iPlayerStaticMaster[client] = -1; - g_hPlayerStaticTimer[client] = INVALID_HANDLE; - g_flPlayerStaticIncreaseRate[client] = 0.0; - g_flPlayerStaticDecreaseRate[client] = 0.0; - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - g_flPlayerLastStaticTime[client] = 0.0; - g_flPlayerLastStaticVolume[client] = 0.0; - g_bPlayerInStaticShake[client] = false; - g_iPlayerStaticShakeMaster[client] = -1; - g_flPlayerStaticShakeMinVolume[client] = 0.0; - g_flPlayerStaticShakeMaxVolume[client] = 0.0; - g_flPlayerStaticAmount[client] = 0.0; - - if (IsClientInGame(client)) - { - if (g_strPlayerStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticSound[client]); - if (g_strPlayerLastStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - if (g_strPlayerStaticShakeSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); - strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), ""); - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); -} - -ClientResetHints(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetHints(%d)", client); -#endif - - for (new i = 0; i < PlayerHint_MaxNum; i++) - { - g_bPlayerHints[client][i] = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetHints(%d)", client); -#endif -} - -ClientShowHint(client, iHint) -{ - g_bPlayerHints[client][iHint] = true; - - switch (iHint) - { - case PlayerHint_Sprint: PrintHintText(client, "%T", "SF2 Hint Sprint", client); - case PlayerHint_Flashlight: PrintHintText(client, "%T", "SF2 Hint Flashlight", client); - case PlayerHint_Blink: PrintHintText(client, "%T", "SF2 Hint Blink", client); - case PlayerHint_MainMenu: PrintHintText(client, "%T", "SF2 Hint Main Menu", client); - } -} - -bool:DidClientEscape(client) -{ - return g_bPlayerEscaped[client]; -} - -ClientEscape(client) -{ - if (DidClientEscape(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("START ClientEscape(%d)", client); -#endif - - g_bPlayerEscaped[client] = true; - - ClientResetBreathing(client); - ClientResetSprint(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientDisableConstantGlow(client); - - // Speed recalculation. Props to the creators of FF2/VSH for this snippet. - TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); - - HandlePlayerHUD(client); - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - CPrintToChatAll("%t", "SF2 Player Escaped", sName); - - CheckRoundWinConditions(); - - Call_StartForward(fOnClientEscape); - Call_PushCell(client); - Call_Finish(); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("END ClientEscape(%d)", client); -#endif -} - -public Action:Timer_TeleportPlayerToEscapePoint(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!DidClientEscape(client)) return; - - if (IsPlayerAlive(client)) - { - TeleportClientToEscapePoint(client); - } -} - -stock Float:ClientGetDistanceFromEntity(client, entity) -{ - decl Float:flStartPos[3], Float:flEndPos[3]; - GetClientAbsOrigin(client, flStartPos); - GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", flEndPos); - return GetVectorDistance(flStartPos, flEndPos); -} - -ClientEnableFakeLagCompensation(client) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client) || g_bPlayerLagCompensation[client]) return; - - // Can only enable lag compensation if we're in either of these two teams only. - new iMyTeam = GetClientTeam(client); - if (iMyTeam != _:TFTeam_Red && iMyTeam != _:TFTeam_Blue) return; - - // Can only enable lag compensation if there are other active teammates around. This is to prevent spontaneous round restarting. - new iCount; - for (new i = 1; i <= MaxClients; i++) - { - if (i == client) continue; - - if (IsValidClient(i) && IsPlayerAlive(i)) - { - new iTeam = GetClientTeam(i); - if ((iTeam == _:TFTeam_Red || iTeam == _:TFTeam_Blue) && iTeam == iMyTeam) - { - iCount++; - } - } - } - - if (!iCount) return; - - // Can only enable lag compensation only for specific weapons. - new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); - if (!IsValidEdict(iActiveWeapon)) return; - - decl String:sClassName[64]; - GetEdictClassname(iActiveWeapon, sClassName, sizeof(sClassName)); - - new bool:bCompensate = false; - for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) - { - if (StrEqual(sClassName, g_strPlayerLagCompensationWeapons[i], false)) - { - bCompensate = true; - break; - } - } - - if (!bCompensate) return; - - g_bPlayerLagCompensation[client] = true; - g_iPlayerLagCompensationTeam[client] = iMyTeam; - SetEntProp(client, Prop_Send, "m_iTeamNum", 0); -} - -ClientDisableFakeLagCompensation(client) -{ - if (!g_bPlayerLagCompensation[client]) return; - - SetEntProp(client, Prop_Send, "m_iTeamNum", g_iPlayerLagCompensationTeam[client]); - g_bPlayerLagCompensation[client] = false; - g_iPlayerLagCompensationTeam[client] = -1; -} - -// ========================================================== -// FLASHLIGHT / ULTRAVISION FUNCTIONS -// ========================================================== - -bool:IsClientUsingFlashlight(client) -{ - return g_bPlayerFlashlight[client]; -} - -Float:ClientGetFlashlightBatteryLife(client) -{ - return g_flPlayerFlashlightBatteryLife[client]; -} - -ClientSetFlashlightBatteryLife(client, Float:flPercent) -{ - g_flPlayerFlashlightBatteryLife[client] = flPercent; -} - -/** - * Called in Hook_ClientPreThink, this makes sure the flashlight is oriented correctly on the player. - */ -static ClientProcessFlashlightAngles(client) -{ - if (!IsClientInGame(client)) return; - - if (IsPlayerAlive(client)) - { - decl fl, Float:eyeAng[3], Float:ang2[3]; - - if (IsClientUsingFlashlight(client)) - { - fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - TeleportEntity(fl, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }, NULL_VECTOR); - } - - fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - GetClientEyeAngles(client, eyeAng); - GetClientAbsAngles(client, ang2); - SubtractVectors(eyeAng, ang2, eyeAng); - TeleportEntity(fl, NULL_VECTOR, eyeAng, NULL_VECTOR); - } - } - } -} - -/** - * Handles whether or not the player's flashlight should be "flickering", a sign of a dying flashlight battery. - */ -static ClientHandleFlashlightFlickerState(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) - { - new bool:bFlicker = bool:(ClientGetFlashlightBatteryLife(client) <= SF2_FLASHLIGHT_FLICKERAT); - - new fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - if (bFlicker) - { - SetEntProp(fl, Prop_Data, "m_LightStyle", 10); - } - else - { - SetEntProp(fl, Prop_Data, "m_LightStyle", 0); - } - } - - fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - if (bFlicker) - { - SetEntityRenderFx(fl, RenderFx:13); - } - else - { - SetEntityRenderFx(fl, RenderFx:0); - } - } - } -} - -bool:IsClientFlashlightBroken(client) -{ - return g_bPlayerFlashlightBroken[client]; -} - -Float:ClientGetFlashlightNextInputTime(client) -{ - return g_flPlayerFlashlightNextInputTime[client]; -} - -/** - * Breaks the player's flashlight. Nothing else. - */ -ClientBreakFlashlight(client) -{ - if (IsClientFlashlightBroken(client)) return; - - g_bPlayerFlashlightBroken[client] = true; - - ClientSetFlashlightBatteryLife(client, 0.0); - ClientTurnOffFlashlight(client); - - ClientAddStress(client, 0.2); - - EmitSoundToAll(FLASHLIGHT_BREAKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - - Call_StartForward(fOnClientBreakFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Resets everything of the player's flashlight. - */ -ClientResetFlashlight(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetFlashlight(%d)", client); -#endif - - ClientTurnOffFlashlight(client); - ClientSetFlashlightBatteryLife(client, 1.0); - g_bPlayerFlashlightBroken[client] = false; - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - g_flPlayerFlashlightNextInputTime[client] = -1.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetFlashlight(%d)", client); -#endif -} - -public Action:Hook_FlashlightSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEnt[other]) != ent) return Plugin_Handled; - - // We've already checked for flashlight ownership in the last statement. So we can do just this. - if (g_iPlayerPreferences[other][PlayerPreference_ProjectedFlashlight]) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Hook_Flashlight2SetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[other]) == ent) return Plugin_Handled; - return Plugin_Continue; -} - -public Hook_FlashlightEndSpawnPost(ent) -{ - if (!g_bEnabled) return; - - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightEndSetTransmit); - SDKUnhook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); -} - -public Action:Hook_FlashlightBeamSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - new iSpotlight = -1; - while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) - { - if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) - { - iOwner = iSpotlight; - break; - } - } - - if (iOwner == -1) return Plugin_Continue; - - new iClient = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) - { - iClient = i; - break; - } - } - - if (iClient == -1) return Plugin_Continue; - - if (iClient == other) - { - if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) - { - return Plugin_Handled; - } - } - /* - else - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - return Plugin_Handled; - } - } - */ - return Plugin_Continue; -} - -public Action:Hook_FlashlightEndSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - new iSpotlight = -1; - while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) - { - if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) - { - iOwner = iSpotlight; - break; - } - } - - if (iOwner == -1) return Plugin_Continue; - - new iClient = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) - { - iClient = i; - break; - } - } - - if (iClient == -1) return Plugin_Continue; - - if (iClient == other) - { - if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) - { - return Plugin_Handled; - } - } - /* - else - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - return Plugin_Handled; - } - } - */ - return Plugin_Continue; -} - -public Action:Timer_DrainFlashlight(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; - - new iOverride = GetConVarInt(g_cvPlayerInfiniteFlashlightOverride); - if ((!g_bRoundInfiniteFlashlight && iOverride != 1) || iOverride == 0) - { - ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) - 0.01); - } - - if (ClientGetFlashlightBatteryLife(client) <= 0.0) - { - // Break the player's flashlight, but also start recharging. - ClientBreakFlashlight(client); - ClientStartRechargingFlashlightBattery(client); - ClientActivateUltravision(client); - return Plugin_Stop; - } - else - { - ClientHandleFlashlightFlickerState(client); - } - - return Plugin_Continue; -} - -public Action:Timer_RechargeFlashlight(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; - - ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) + 0.01); - - if (IsClientFlashlightBroken(client) && ClientGetFlashlightBatteryLife(client) >= SF2_FLASHLIGHT_ENABLEAT) - { - // Repair the flashlight. - g_bPlayerFlashlightBroken[client] = false; - } - - if (ClientGetFlashlightBatteryLife(client) >= 1.0) - { - // I am fully charged! - ClientSetFlashlightBatteryLife(client, 1.0); - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - - return Plugin_Stop; - } - - return Plugin_Continue; -} - -/** - * Turns on the player's flashlight. Nothing else. - */ -ClientTurnOnFlashlight(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) return; - - g_bPlayerFlashlight[client] = true; - - decl Float:flEyePos[3]; - GetClientEyePosition(client, flEyePos); - - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - // If the player is using the projected flashlight, just set effect flags. - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (!(iEffects & (1 << 2))) - { - SetEntProp(client, Prop_Send, "m_fEffects", iEffects | (1 << 2)); - } - } - else - { - // Spawn the light which only the user will see. - new ent = CreateEntityByName("light_dynamic"); - if (ent != -1) - { - TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); - DispatchKeyValue(ent, "targetname", "WUBADUBDUBMOTHERBUCKERS"); - DispatchKeyValue(ent, "rendercolor", "255 255 255"); - SetVariantFloat(SF2_FLASHLIGHT_WIDTH); - AcceptEntityInput(ent, "spotlight_radius"); - SetVariantFloat(SF2_FLASHLIGHT_LENGTH); - AcceptEntityInput(ent, "distance"); - SetVariantInt(SF2_FLASHLIGHT_BRIGHTNESS); - AcceptEntityInput(ent, "brightness"); - - // Convert WU to inches. - new Float:cone = 55.0; - cone *= 0.75; - - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_inner_cone"); - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_cone"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "TurnOn"); - - g_iPlayerFlashlightEnt[client] = EntIndexToEntRef(ent); - - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightSetTransmit); - } - } - - // Spawn the light that only everyone else will see. - new ent = CreateEntityByName("point_spotlight"); - if (ent != -1) - { - TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); - - decl String:sBuffer[256]; - FloatToString(SF2_FLASHLIGHT_LENGTH, sBuffer, sizeof(sBuffer)); - DispatchKeyValue(ent, "spotlightlength", sBuffer); - FloatToString(SF2_FLASHLIGHT_WIDTH, sBuffer, sizeof(sBuffer)); - DispatchKeyValue(ent, "spotlightwidth", sBuffer); - DispatchKeyValue(ent, "rendercolor", "255 255 255"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "LightOn"); - - g_iPlayerFlashlightEntAng[client] = EntIndexToEntRef(ent); - } - - Call_StartForward(fOnClientActivateFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Turns off the player's flashlight. Nothing else. - */ -ClientTurnOffFlashlight(client) -{ - if (!IsClientUsingFlashlight(client)) return; - - g_bPlayerFlashlight[client] = false; - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - - // Remove user-only light. - new ent = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "TurnOff"); - AcceptEntityInput(ent, "Kill"); - } - - // Remove everyone-else-only light. - ent = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "LightOff"); - CreateTimer(0.1, Timer_KillEntity, g_iPlayerFlashlightEntAng[client], TIMER_FLAG_NO_MAPCHANGE); - } - - g_iPlayerFlashlightEnt[client] = INVALID_ENT_REFERENCE; - g_iPlayerFlashlightEntAng[client] = INVALID_ENT_REFERENCE; - - if (IsClientInGame(client)) - { - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (iEffects & (1 << 2)) - { - SetEntProp(client, Prop_Send, "m_fEffects", iEffects &= ~(1 << 2)); - } - } - } - - Call_StartForward(fOnClientDeactivateFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -ClientStartRechargingFlashlightBattery(client) -{ - g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(SF2_FLASHLIGHT_RECHARGE_RATE, Timer_RechargeFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStartDrainingFlashlightBattery(client) -{ - new Float:flDrainRate = SF2_FLASHLIGHT_DRAIN_RATE; - if (TF2_GetPlayerClass(client) == TFClass_Engineer) - { - // Engineers have a 33% longer battery life, basically. - // TODO: Make this value customizable via cvar. - flDrainRate *= 1.33; - } - - g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(flDrainRate, Timer_DrainFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -ClientHandleFlashlight(client) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) - { - ClientTurnOffFlashlight(client); - ClientStartRechargingFlashlightBattery(client); - ClientActivateUltravision(client); - - g_flPlayerFlashlightNextInputTime[client] = GetGameTime() + SF2_FLASHLIGHT_COOLDOWN; - - EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - } - else - { - // Only players in the "game" can use the flashlight. - if (!g_bPlayerEliminated[client]) - { - new bool:bCanUseFlashlight = true; - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_LIGHTSOUT) - { - // Unequip the flashlight please. - bCanUseFlashlight = false; - } - - if (!IsClientFlashlightBroken(client) && bCanUseFlashlight) - { - ClientTurnOnFlashlight(client); - ClientStartDrainingFlashlightBattery(client); - ClientDeactivateUltravision(client); - - g_flPlayerFlashlightNextInputTime[client] = GetGameTime(); - - EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - } - else - { - EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); - } - } - } -} - -bool:IsClientUsingUltravision(client) -{ - return g_bPlayerUltravision[client]; -} - -ClientActivateUltravision(client) -{ - if (!IsClientInGame(client) || IsClientUsingUltravision(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientActivateUltravision(%d)", client); -#endif - - g_bPlayerUltravision[client] = true; - - new ent = CreateEntityByName("light_dynamic"); - if (ent != -1) - { - decl Float:flEyePos[3]; - GetClientEyePosition(client, flEyePos); - - TeleportEntity(ent, flEyePos, Float:{ 90.0, 0.0, 0.0 }, NULL_VECTOR); - DispatchKeyValue(ent, "rendercolor", "0 200 255"); - - new Float:flRadius = 0.0; - if (g_bPlayerEliminated[client]) - { - flRadius = GetConVarFloat(g_cvUltravisionRadiusBlue); - } - else - { - flRadius = GetConVarFloat(g_cvUltravisionRadiusRed); - } - - SetVariantFloat(flRadius); - AcceptEntityInput(ent, "spotlight_radius"); - SetVariantFloat(flRadius); - AcceptEntityInput(ent, "distance"); - - SetVariantInt(-15); // Start dark, then fade in via the Timer_UltravisionFadeInEffect timer func. - AcceptEntityInput(ent, "brightness"); - - // Convert WU to inches. - new Float:cone = SF2_ULTRAVISION_CONE; - cone *= 0.75; - - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_inner_cone"); - SetVariantInt(0); - AcceptEntityInput(ent, "_cone"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "TurnOn"); - SetEntityRenderFx(ent, RENDERFX_SOLID_SLOW); - SetEntityRenderColor(ent, 100, 200, 255, 255); - - g_iPlayerUltravisionEnt[client] = EntIndexToEntRef(ent); - - SDKHook(ent, SDKHook_SetTransmit, Hook_UltravisionSetTransmit); - - // Fade in effect. - CreateTimer(0.0, Timer_UltravisionFadeInEffect, g_iPlayerUltravisionEnt[client], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientActivateUltravision(%d)", client); -#endif -} - -public Action:Timer_UltravisionFadeInEffect(Handle:timer, any:entref) -{ - new ent = EntRefToEntIndex(entref); - if (!ent || ent == INVALID_ENT_REFERENCE) return Plugin_Stop; - - new iBrightness = GetEntProp(ent, Prop_Send, "m_Exponent"); - if (iBrightness >= GetConVarInt(g_cvUltravisionBrightness)) return Plugin_Stop; - - iBrightness++; - SetVariantInt(iBrightness); - AcceptEntityInput(ent, "brightness"); - - return Plugin_Continue; -} - -ClientDeactivateUltravision(client) -{ - if (!IsClientUsingUltravision(client)) return; - - g_bPlayerUltravision[client] = false; - - new ent = EntRefToEntIndex(g_iPlayerUltravisionEnt[client]); - if (ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "TurnOff"); - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerUltravisionEnt[client] = INVALID_ENT_REFERENCE; -} - -public Action:Hook_UltravisionSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!GetConVarBool(g_cvUltravisionEnabled) || EntRefToEntIndex(g_iPlayerUltravisionEnt[other]) != ent || !IsPlayerAlive(other)) return Plugin_Handled; - return Plugin_Continue; -} - -static Float:ClientGetDefaultWalkSpeed(client) -{ - new Float:flReturn = 190.0; - new Float:flReturn2 = flReturn; - new Action:iAction = Plugin_Continue; - new TFClassType:iClass = TF2_GetPlayerClass(client); - - switch (iClass) - { - case TFClass_Scout: flReturn = 190.0; - case TFClass_Sniper: flReturn = 190.0; - case TFClass_Soldier: flReturn = 190.0; - case TFClass_DemoMan: flReturn = 190.0; - case TFClass_Heavy: flReturn = 190.0; - case TFClass_Medic: flReturn = 190.0; - case TFClass_Pyro: flReturn = 190.0; - case TFClass_Spy: flReturn = 190.0; - case TFClass_Engineer: flReturn = 190.0; - } - - // Call our forward. - Call_StartForward(fOnClientGetDefaultWalkSpeed); - Call_PushCell(client); - Call_PushCellRef(flReturn2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) flReturn = flReturn2; - - return flReturn; -} - -static Float:ClientGetDefaultSprintSpeed(client) -{ - new Float:flReturn = 300.0; - new Float:flReturn2 = flReturn; - new Action:iAction = Plugin_Continue; - new TFClassType:iClass = TF2_GetPlayerClass(client); - - switch (iClass) - { - case TFClass_Scout: flReturn = 300.0; - case TFClass_Sniper: flReturn = 300.0; - case TFClass_Soldier: flReturn = 275.0; - case TFClass_DemoMan: flReturn = 285.0; - case TFClass_Heavy: flReturn = 270.0; - case TFClass_Medic: flReturn = 300.0; - case TFClass_Pyro: flReturn = 300.0; - case TFClass_Spy: flReturn = 300.0; - case TFClass_Engineer: flReturn = 300.0; - } - - // Call our forward. - Call_StartForward(fOnClientGetDefaultSprintSpeed); - Call_PushCell(client); - Call_PushCellRef(flReturn2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) flReturn = flReturn2; - - return flReturn; -} - -// Static shaking should only affect the x, y portion of the player's view, not roll. -// This is purely for cosmetic effect. - -ClientProcessStaticShake(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - new bool:bOldStaticShake = g_bPlayerInStaticShake[client]; - new iOldStaticShakeMaster = NPCGetFromUniqueID(g_iPlayerStaticShakeMaster[client]); - new iNewStaticShakeMaster = -1; - new Float:flNewStaticShakeMasterAnger = -1.0; - - new Float:flOldPunchAng[3], Float:flOldPunchAngVel[3]; - GetEntDataVector(client, g_offsPlayerPunchAngle, flOldPunchAng); - GetEntDataVector(client, g_offsPlayerPunchAngleVel, flOldPunchAngVel); - - new Float:flNewPunchAng[3], Float:flNewPunchAngVel[3]; - - for (new i = 0; i < 3; i++) - { - flNewPunchAng[i] = flOldPunchAng[i]; - flNewPunchAngVel[i] = flOldPunchAngVel[i]; - } - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (g_iPlayerStaticMode[client][i] != Static_Increase) continue; - if (!(NPCGetFlags(i) & SFF_HASSTATICSHAKE)) continue; - - if (NPCGetAnger(i) > flNewStaticShakeMasterAnger) - { - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - if (iMaster == -1) iMaster = i; - - iNewStaticShakeMaster = iMaster; - flNewStaticShakeMasterAnger = NPCGetAnger(iMaster); - } - } - - if (iNewStaticShakeMaster != -1) - { - g_iPlayerStaticShakeMaster[client] = NPCGetUniqueID(iNewStaticShakeMaster); - - if (iNewStaticShakeMaster != iOldStaticShakeMaster) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iNewStaticShakeMaster, sProfile, sizeof(sProfile)); - - if (g_strPlayerStaticShakeSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - g_flPlayerStaticShakeMinVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_min", 0.0); - g_flPlayerStaticShakeMaxVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_max", 1.0); - - decl String:sStaticSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_shake_local", sStaticSound, sizeof(sStaticSound)); - if (sStaticSound[0]) - { - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), sStaticSound); - } - else - { - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); - } - } - } - - if (g_bPlayerInStaticShake[client]) - { - if (g_flPlayerStaticAmount[client] <= 0.0) - { - g_bPlayerInStaticShake[client] = false; - } - } - else - { - if (iNewStaticShakeMaster != -1) - { - g_bPlayerInStaticShake[client] = true; - } - } - - if (g_bPlayerInStaticShake[client] && !bOldStaticShake) - { - for (new i = 0; i < 2; i++) - { - flNewPunchAng[i] = 0.0; - flNewPunchAngVel[i] = 0.0; - } - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } - else if (!g_bPlayerInStaticShake[client] && bOldStaticShake) - { - for (new i = 0; i < 2; i++) - { - flNewPunchAng[i] = 0.0; - flNewPunchAngVel[i] = 0.0; - } - - g_iPlayerStaticShakeMaster[client] = -1; - - if (g_strPlayerStaticShakeSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); - - g_flPlayerStaticShakeMinVolume[client] = 0.0; - g_flPlayerStaticShakeMaxVolume[client] = 0.0; - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } - - if (g_bPlayerInStaticShake[client]) - { - if (g_strPlayerStaticShakeSound[client][0]) - { - new Float:flVolume = g_flPlayerStaticAmount[client]; - if (GetRandomFloat(0.0, 1.0) <= 0.35) - { - flVolume = 0.0; - } - else - { - if (flVolume < g_flPlayerStaticShakeMinVolume[client]) - { - flVolume = g_flPlayerStaticShakeMinVolume[client]; - } - - if (flVolume > g_flPlayerStaticShakeMaxVolume[client]) - { - flVolume = g_flPlayerStaticShakeMaxVolume[client]; - } - } - - EmitSoundToClient(client, g_strPlayerStaticShakeSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL | SND_STOP, flVolume); - } - - // Spazz our view all over the place. - for (new i = 0; i < 2; i++) flNewPunchAng[i] = AngleNormalize(GetRandomFloat(0.0, 360.0)); - NormalizeVector(flNewPunchAng, flNewPunchAng); - - new Float:flAngVelocityScalar = 5.0 * g_flPlayerStaticAmount[client]; - if (flAngVelocityScalar < 1.0) flAngVelocityScalar = 1.0; - ScaleVector(flNewPunchAng, flAngVelocityScalar); - - for (new i = 0; i < 2; i++) flNewPunchAngVel[i] = 0.0; - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } -} - -ClientProcessVisibility(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - new String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - new bool:bWasSeeingSlender[MAX_BOSSES]; - new iOldStaticMode[MAX_BOSSES]; - - decl Float:flSlenderPos[3]; - decl Float:flSlenderEyePos[3]; - decl Float:flSlenderOBBCenterPos[3]; - - decl Float:flMyPos[3]; - GetClientAbsOrigin(client, flMyPos); - - for (new i = 0; i < MAX_BOSSES; i++) - { - bWasSeeingSlender[i] = g_bPlayerSeesSlender[client][i]; - iOldStaticMode[i] = g_iPlayerStaticMode[client][i]; - g_bPlayerSeesSlender[client][i] = false; - g_iPlayerStaticMode[client][i] = Static_None; - - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - new iBoss = NPCGetEntIndex(i); - - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - SlenderGetAbsOrigin(i, flSlenderPos); - NPCGetEyePosition(i, flSlenderEyePos); - - decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; - GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flSlenderMins); - GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flSlenderMaxs); - - for (new i2 = 0; i2 < 3; i2++) flSlenderOBBCenterPos[i2] = flSlenderPos[i2] + ((flSlenderMins[i2] + flSlenderMaxs[i2]) / 2.0); - } - - if (IsClientInGhostMode(client)) - { - } - else if (!IsClientInDeathCam(client)) - { - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - - if (!IsPointVisibleToPlayer(client, flSlenderEyePos, true, SlenderUsesBlink(i))) - { - g_bPlayerSeesSlender[client][i] = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, true, SlenderUsesBlink(i)); - } - else - { - g_bPlayerSeesSlender[client][i] = true; - } - - if ((GetGameTime() - g_flPlayerSeesSlenderLastTime[client][i]) > GetProfileFloat(sProfile, "static_on_look_gracetime", 1.0) || - (iOldStaticMode[i] == Static_Increase && g_flPlayerStaticAmount[client] > 0.1)) - { - if ((NPCGetFlags(i) & SFF_STATICONLOOK) && - g_bPlayerSeesSlender[client][i]) - { - if (iCopyMaster != -1) - { - g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; - } - else - { - g_iPlayerStaticMode[client][i] = Static_Increase; - } - } - else if ((NPCGetFlags(i) & SFF_STATICONRADIUS) && - GetVectorDistance(flMyPos, flSlenderPos) <= g_flSlenderStaticRadius[i]) - { - new bool:bNoObstacles = IsPointVisibleToPlayer(client, flSlenderEyePos, false, false); - if (!bNoObstacles) bNoObstacles = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, false); - - if (bNoObstacles) - { - if (iCopyMaster != -1) - { - g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; - } - else - { - g_iPlayerStaticMode[client][i] = Static_Increase; - } - } - } - } - - // Process death cam sequence conditions - if (SlenderKillsOnNear(i)) - { - if (g_flPlayerStaticAmount[client] >= 1.0 || - GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetInstantKillRadius(i)) - { - new bool:bKillPlayer = true; - if (g_flPlayerStaticAmount[client] < 1.0) - { - bKillPlayer = IsPointVisibleToPlayer(client, flSlenderEyePos, false, SlenderUsesBlink(i)); - } - - if (!bKillPlayer) bKillPlayer = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, SlenderUsesBlink(i)); - - if (bKillPlayer) - { - g_flSlenderLastKill[i] = GetGameTime(); - - if (g_flPlayerStaticAmount[client] >= 1.0) - { - ClientStartDeathCam(client, NPCGetFromUniqueID(g_iPlayerStaticMaster[client]), flSlenderPos); - } - else - { - ClientStartDeathCam(client, i, flSlenderPos); - } - } - } - } - } - } - - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - if (iMaster == -1) iMaster = i; - - // Boss visiblity. - if (g_bPlayerSeesSlender[client][i] && !bWasSeeingSlender[i]) - { - g_flPlayerSeesSlenderLastTime[client][iMaster] = GetGameTime(); - - if (GetGameTime() >= g_flPlayerScareNextTime[client][iMaster]) - { - if (GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetScareRadius(i)) - { - ClientPerformScare(client, iMaster); - - if (NPCHasAttribute(iMaster, "ignite player on scare")) - { - new Float:flValue = NPCGetAttributeValue(iMaster, "ignite player on scare"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - } - else - { - g_flPlayerScareNextTime[client][iMaster] = GetGameTime() + GetProfileFloat(sProfile, "scare_cooldown"); - } - } - - if (NPCGetType(i) == SF2BossType_Static) - { - if (NPCGetFlags(i) & SFF_FAKE) - { - SlenderMarkAsFake(i); - return; - } - } - - Call_StartForward(fOnClientLooksAtBoss); - Call_PushCell(client); - Call_PushCell(i); - Call_Finish(); - } - else if (!g_bPlayerSeesSlender[client][i] && bWasSeeingSlender[i]) - { - g_flPlayerScareLastTime[client][iMaster] = GetGameTime(); - - Call_StartForward(fOnClientLooksAwayFromBoss); - Call_PushCell(client); - Call_PushCell(i); - Call_Finish(); - } - - if (g_bPlayerSeesSlender[client][i]) - { - if (GetGameTime() >= g_flPlayerSightSoundNextTime[client][iMaster]) - { - ClientPerformSightSound(client, i); - } - } - - if (g_iPlayerStaticMode[client][i] == Static_Increase && - iOldStaticMode[i] != Static_Increase) - { - if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) - { - decl String:sLoopSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); - - if (sLoopSound[0]) - { - EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, GetProfileNum(sProfile, "sound_static_loop_local_level", SNDLEVEL_NORMAL), SND_CHANGEVOL, 1.0); - ClientAddStress(client, 0.03); - } - else - { - LogError("Warning! Boss %s supports static loop local sounds, but was given a blank sound path!", sProfile); - } - } - } - else if (g_iPlayerStaticMode[client][i] != Static_Increase && - iOldStaticMode[i] == Static_Increase) - { - if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) - { - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - decl String:sLoopSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); - - if (sLoopSound[0]) - { - EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, _, SND_CHANGEVOL | SND_STOP, 0.0); - } - } - } - } - } - - // Initialize static timers. - new iBossLastStatic = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); - new iBossNewStatic = -1; - if (iBossLastStatic != -1 && g_iPlayerStaticMode[client][iBossLastStatic] == Static_Increase) - { - iBossNewStatic = iBossLastStatic; - } - - for (new i = 0; i < MAX_BOSSES; i++) - { - new iStaticMode = g_iPlayerStaticMode[client][i]; - - // Determine new static rates. - if (iStaticMode != Static_Increase) continue; - - if (iBossLastStatic == -1 || - g_iPlayerStaticMode[client][iBossLastStatic] != Static_Increase || - NPCGetAnger(i) > NPCGetAnger(iBossLastStatic)) - { - iBossNewStatic = i; - } - } - - if (iBossNewStatic != -1) - { - new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossNewStatic]); - if (iCopyMaster != -1) - { - iBossNewStatic = iCopyMaster; - g_iPlayerStaticMaster[client] = NPCGetUniqueID(iCopyMaster); - } - else - { - g_iPlayerStaticMaster[client] = NPCGetUniqueID(iBossNewStatic); - } - } - else - { - g_iPlayerStaticMaster[client] = -1; - } - - if (iBossNewStatic != iBossLastStatic) - { - if (!StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) - { - // Stop last-last static sound entirely. - if (g_strPlayerLastStaticSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - } - } - - // Move everything down towards the last arrays. - if (g_strPlayerStaticSound[client][0]) - { - strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), g_strPlayerStaticSound[client]); - } - - if (iBossNewStatic == -1) - { - // No one is the static master. - g_hPlayerStaticTimer[client] = CreateTimer(g_flPlayerStaticDecreaseRate[client], - Timer_ClientDecreaseStatic, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerStaticTimer[client], true); - } - else - { - NPCGetProfile(iBossNewStatic, sProfile, sizeof(sProfile)); - - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); - - new String:sStaticSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static", sStaticSound, sizeof(sStaticSound), 1); - - if (sStaticSound[0]) - { - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), sStaticSound); - } - - // Cross-fade out the static sounds. - g_flPlayerLastStaticVolume[client] = g_flPlayerStaticAmount[client]; - g_flPlayerLastStaticTime[client] = GetGameTime(); - - g_hPlayerLastStaticTimer[client] = CreateTimer(0.0, - Timer_ClientFadeOutLastStaticSound, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerLastStaticTimer[client], true); - - // Start up our own static timer. - new Float:flStaticIncreaseRate = GetProfileFloat(sProfile, "static_rate") / g_flRoundDifficultyModifier; - new Float:flStaticDecreaseRate = GetProfileFloat(sProfile, "static_rate_decay"); - - g_flPlayerStaticIncreaseRate[client] = flStaticIncreaseRate; - g_flPlayerStaticDecreaseRate[client] = flStaticDecreaseRate; - - g_hPlayerStaticTimer[client] = CreateTimer(flStaticIncreaseRate, - Timer_ClientIncreaseStatic, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerStaticTimer[client], true); - } - } -} - -ClientProcessViewAngles(client) -{ - if ((!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) && - !DidClientEscape(client)) - { - // Process view bobbing, if enabled. - // This code is based on the code in this page: https://developer.valvesoftware.com/wiki/Camera_Bob - // Many thanks to whomever created it in the first place. - - if (IsPlayerAlive(client)) - { - if (g_bPlayerViewbobEnabled) - { - new Float:flPunchVel[3]; - - if (!g_bPlayerViewbobSprintEnabled || !IsClientReallySprinting(client)) - { - if (GetEntityFlags(client) & FL_ONGROUND) - { - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - new Float:flSpeed = GetVectorLength(flVelocity); - - new Float:flPunchIdle[3]; - - if (flSpeed > 0.0) - { - if (flSpeed >= 60.0) - { - flPunchIdle[0] = Sine(GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_X / 400.0; - flPunchIdle[1] = Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Y / 400.0; - flPunchIdle[2] = Sine(1.6 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - - // Calculate roll. - decl Float:flForward[3], Float:flVelocityDirection[3]; - GetClientEyeAngles(client, flForward); - GetVectorAngles(flVelocity, flVelocityDirection); - - new Float:flYawDiff = AngleDiff(flForward[1], flVelocityDirection[1]); - if (FloatAbs(flYawDiff) > 90.0) flYawDiff = AngleDiff(flForward[1] + 180.0, flVelocityDirection[1]) * -1.0; - - new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); - new Float:flRollScalar = flSpeed / flWalkSpeed; - if (flRollScalar > 1.0) flRollScalar = 1.0; - - new Float:flRollScale = (flYawDiff / 90.0) * 0.25 * flRollScalar; - flPunchIdle[0] = 0.0; - flPunchIdle[1] = 0.0; - flPunchIdle[2] = flRollScale * -1.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - - g_flPlayerBreathViewbobPhase[client] += SF2_BREATH_VIEWBOB_SPEED; - if(g_flPlayerBreathViewbobPhase[client] > 3.14159265355) { - g_flPlayerBreathViewbobPhase[client] = 0.0; // Sine cycle - g_flPlayerBreathViewbobXMult[client] = GetRandomFloat(-1.0, 1.0); - g_flPlayerBreathViewbobYMult[client] = GetRandomFloat(-1.0, 1.0); - } - new Float:sine = Sine(g_flPlayerBreathViewbobPhase[client]); - flPunchIdle[0] = SF2_BREATH_VIEWBOB_START * sine * g_flPlayerBreathViewbobXMult[client] + SF2_BREATH_VIEWBOB_AMPLITUDE * sine * g_flPlayerBreathViewbobXMult[client] * float(100 - g_iPlayerSprintPoints[client]) / 100.0; - flPunchIdle[1] = SF2_BREATH_VIEWBOB_START / 2.0 * sine * g_flPlayerBreathViewbobYMult[client] + SF2_BREATH_VIEWBOB_AMPLITUDE / 2.0 * sine * g_flPlayerBreathViewbobYMult[client] * float(100 - g_iPlayerSprintPoints[client]) / 100.0; - flPunchIdle[2] = 0.0;//Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) ;//Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - /* - if (flSpeed < 60.0) - { - flPunchIdle[0] = FloatAbs(Cosine(GetGameTime() * 1.25) * 0.047); - flPunchIdle[1] = Sine(GetGameTime() * 1.25) * 0.075; - flPunchIdle[2] = 0.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - */ - } - } - - if (g_bPlayerViewbobHurtEnabled) - { - // Shake screen the more the player is hurt. - new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); - new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); - - decl Float:flPunchVelHurt[3]; - flPunchVelHurt[0] = Sine(1.22 * GetGameTime()) * 48.5 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - flPunchVelHurt[1] = Sine(2.12 * GetGameTime()) * 80.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - flPunchVelHurt[2] = Sine(0.5 * GetGameTime()) * 36.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - - AddVectors(flPunchVel, flPunchVelHurt, flPunchVel); - } - - ClientViewPunch(client, flPunchVel); - } - } - } -} - -public Action:Timer_ClientIncreaseStatic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; - - g_flPlayerStaticAmount[client] += 0.05; - if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; - - if (g_strPlayerStaticSound[client][0]) - { - EmitSoundToClient(client, g_strPlayerStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerStaticAmount[client]); - - if (g_flPlayerStaticAmount[client] >= 0.5) ClientAddStress(client, 0.03); - else - { - ClientAddStress(client, 0.02); - } - } - - return Plugin_Continue; -} - -public Action:Timer_ClientDecreaseStatic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; - - g_flPlayerStaticAmount[client] -= 0.05; - if (g_flPlayerStaticAmount[client] < 0.0) g_flPlayerStaticAmount[client] = 0.0; - - if (g_strPlayerLastStaticSound[client][0]) - { - new Float:flVolume = g_flPlayerStaticAmount[client]; - if (flVolume > 0.0) - { - EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); - } - } - - if (g_flPlayerStaticAmount[client] <= 0.0 && g_strPlayerLastStaticSound[client][0]) - { - // I've done my job; no point to keep on doing it. - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - g_hPlayerStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_ClientFadeOutLastStaticSound(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerLastStaticTimer[client]) return Plugin_Stop; - - if (StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) - { - // Wait, the player's current static sound is the same one we're stopping. Abort! - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - if (g_strPlayerLastStaticSound[client][0]) - { - new Float:flDiff = (GetGameTime() - g_flPlayerLastStaticTime[client]) / 1.0; - if (flDiff > 1.0) flDiff = 1.0; - - new Float:flVolume = g_flPlayerLastStaticVolume[client] - flDiff; - if (flVolume < 0.0) flVolume = 0.0; - - if (flVolume <= 0.0) - { - // I've done my job; no point to keep on doing it. - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - else - { - EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); - } - } - else - { - // I've done my job; no point to keep on doing it. - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -// ========================================================== -// INTERACTIVE GLOW FUNCTIONS -// ========================================================== - -static ClientProcessInteractiveGlow(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client) || (g_bPlayerEliminated[client] && !g_bPlayerProxy[client]) || IsClientInGhostMode(client)) return; - - new iOldLookEntity = EntRefToEntIndex(g_iPlayerInteractiveGlowTargetEntity[client]); - - decl Float:flStartPos[3], Float:flMyEyeAng[3]; - GetClientEyePosition(client, flStartPos); - GetClientEyeAngles(client, flMyEyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flMyEyeAng, MASK_VISIBLE, RayType_Infinite, TraceRayDontHitPlayers, -1); - new iEnt = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (IsValidEntity(iEnt)) - { - g_iPlayerInteractiveGlowTargetEntity[client] = EntRefToEntIndex(iEnt); - } - else - { - g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; - } - - if (iEnt != iOldLookEntity) - { - ClientRemoveInteractiveGlow(client); - - if (IsEntityClassname(iEnt, "prop_dynamic", false)) - { - decl String:sTargetName[64]; - GetEntPropString(iEnt, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); - - if (StrContains(sTargetName, "sf2_page", false) == 0 || StrContains(sTargetName, "sf2_interact", false) == 0) - { - ClientCreateInteractiveGlow(client, iEnt); - } - } - } -} - -ClientResetInteractiveGlow(client) -{ - ClientRemoveInteractiveGlow(client); - g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; -} - -/** - * Removes the player's current interactive glow entity. - */ -ClientRemoveInteractiveGlow(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientRemoveInteractiveGlow(%d)", client); -#endif - - new ent = EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerInteractiveGlowEntity[client] = INVALID_ENT_REFERENCE; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientRemoveInteractiveGlow(%d)", client); -#endif -} - -/** - * Creates an interactive glow for an entity to show to a player. - */ -bool:ClientCreateInteractiveGlow(client, iEnt, const String:sAttachment[]="") -{ - ClientRemoveInteractiveGlow(client); - - if (!IsClientInGame(client)) return false; - - if (!iEnt || !IsValidEdict(iEnt)) return false; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientCreateInteractiveGlow(%d)", client); -#endif - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetEntPropString(iEnt, Prop_Data, "m_ModelName", sBuffer, sizeof(sBuffer)); - - if (strlen(sBuffer) == 0) - { - return false; - } - - new ent = CreateEntityByName("tf_taunt_prop"); - if (ent != -1) - { - g_iPlayerInteractiveGlowEntity[client] = EntIndexToEntRef(ent); - - new Float:flModelScale = GetEntPropFloat(iEnt, Prop_Send, "m_flModelScale"); - - SetEntityModel(ent, sBuffer); - DispatchSpawn(ent); - ActivateEntity(ent); - SetEntityRenderMode(ent, RENDER_TRANSCOLOR); - SetEntityRenderColor(ent, 0, 0, 0, 0); - SetEntProp(ent, Prop_Send, "m_bGlowEnabled", 1); - SetEntPropFloat(ent, Prop_Send, "m_flModelScale", flModelScale); - - new iFlags = GetEntProp(ent, Prop_Send, "m_fEffects"); - SetEntProp(ent, Prop_Send, "m_fEffects", iFlags | (1 << 0)); - - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", iEnt); - - if (sAttachment[0]) - { - SetVariantString(sAttachment); - AcceptEntityInput(ent, "SetParentAttachment"); - } - - SDKHook(ent, SDKHook_SetTransmit, Hook_InterativeGlowSetTransmit); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> true", client); -#endif - - return true; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> false", client); -#endif - - return false; -} - -public Action:Hook_InterativeGlowSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[other]) != ent) return Plugin_Handled; - - return Plugin_Continue; -} - -// ========================================================== -// BREATHING FUNCTIONS -// ========================================================== - -ClientResetBreathing(client) -{ - g_bPlayerBreath[client] = false; - g_hPlayerBreathTimer[client] = INVALID_HANDLE; -} - -Float:ClientCalculateBreathingCooldown(client) -{ - new Float:flAverage = 0.0; - new iAverageNum = 0; - - // Sprinting only, for now. - flAverage += (SF2_PLAYER_BREATH_COOLDOWN_MAX * 6.7765 * Pow((float(g_iPlayerSprintPoints[client]) / 100.0), 1.65)); - iAverageNum++; - - flAverage /= float(iAverageNum) - - if (flAverage < SF2_PLAYER_BREATH_COOLDOWN_MIN) flAverage = SF2_PLAYER_BREATH_COOLDOWN_MIN; - - return flAverage; -} - -ClientStartBreathing(client) -{ - g_bPlayerBreath[client] = true; - g_hPlayerBreathTimer[client] = CreateTimer(ClientCalculateBreathingCooldown(client), Timer_ClientBreath, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStopBreathing(client) -{ - g_bPlayerBreath[client] = false; - g_hPlayerBreathTimer[client] = INVALID_HANDLE; -} - -bool:ClientCanBreath(client) -{ - return bool:(ClientCalculateBreathingCooldown(client) < SF2_PLAYER_BREATH_COOLDOWN_MAX); -} - -public Action:Timer_ClientBreath(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerBreathTimer[client]) return; - - if (!g_bPlayerBreath[client]) return; - - if (ClientCanBreath(client)) - { - EmitSoundToAll(g_strPlayerBreathSounds[GetRandomInt(0, sizeof(g_strPlayerBreathSounds) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - - ClientStartBreathing(client); - return; - } - - ClientStopBreathing(client); -} - -// ========================================================== -// SPRINTING FUNCTIONS -// ========================================================== - -bool:IsClientSprinting(client) -{ - return g_bPlayerSprint[client]; -} - -ClientGetSprintPoints(client) -{ - return g_iPlayerSprintPoints[client]; -} - -ClientResetSprint(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSprint(%d)", client); -#endif - - g_bPlayerSprint[client] = false; - g_iPlayerSprintPoints[client] = 100; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - - if (IsValidClient(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSprint(%d)", client); -#endif -} - -ClientStartSprint(client) -{ - if (IsClientSprinting(client)) return; - - g_bPlayerSprint[client] = true; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - ClientSprintTimer(client); - TriggerTimer(g_hPlayerSprintTimer[client], true); - - SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); -} - -static ClientSprintTimer(client, bool:bRecharge=false) -{ - new Float:flRate = 0.28; - if (bRecharge) flRate = 0.8; - - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - - if (bRecharge) - { - if (!(GetEntityFlags(client) & FL_ONGROUND)) flRate *= 0.75; - else if (GetVectorLength(flVelocity) == 0.0) - { - if (GetEntProp(client, Prop_Send, "m_bDucked")) flRate *= 0.66; - else flRate *= 0.75; - } - } - else - { - if (TF2_GetPlayerClass(client) == TFClass_Scout) flRate *= 1.15; - } - - if (bRecharge) g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientRechargeSprint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - else g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientSprinting, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStopSprint(client) -{ - if (!IsClientSprinting(client)) return; - - g_bPlayerSprint[client] = false; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - ClientSprintTimer(client, true); - - SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); -} - -bool:IsClientReallySprinting(client) -{ - if (!IsClientSprinting(client)) return false; - if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; - - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - if (GetVectorLength(flVelocity) < 30.0) return false; - - return true; -} - -public Action:Timer_ClientSprinting(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSprintTimer[client]) return; - - if (!IsClientSprinting(client)) return; - - if (g_iPlayerSprintPoints[client] <= 0) - { - ClientStopSprint(client); - g_iPlayerSprintPoints[client] = 0; - return; - } - - if (IsClientReallySprinting(client)) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); - if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) - { - g_iPlayerSprintPoints[client]--; - } - } - - ClientSprintTimer(client); -} - -public Hook_ClientSprintingPreThink(client) -{ - if (!IsClientReallySprinting(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - return; - } - /* - new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); - - new iTargetFOV = g_iPlayerDesiredFOV[client] + 10; - - if (iFOV < iTargetFOV) - { - new iDiff = RoundFloat(FloatAbs(float(iFOV - iTargetFOV))); - if (iDiff >= 1) - { - ClientSetFOV(client, iFOV + 1); - } - else - { - ClientSetFOV(client, iTargetFOV); - } - } - else if (iFOV >= iTargetFOV) - { - ClientSetFOV(client, iTargetFOV); - //SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - } - */ -} - -public Hook_ClientRechargeSprintPreThink(client) -{ - if (IsClientReallySprinting(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - return; - } - - new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); - if (iFOV > g_iPlayerDesiredFOV[client]) - { - new iDiff = RoundFloat(FloatAbs(float(iFOV - g_iPlayerDesiredFOV[client]))); - if (iDiff >= 1) - { - ClientSetFOV(client, iFOV - 1); - } - else - { - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } - } - else if (iFOV <= g_iPlayerDesiredFOV[client]) - { - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } -} - -public Action:Timer_ClientRechargeSprint(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSprintTimer[client]) return; - - if (IsClientSprinting(client)) - { - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - return; - } - - if (g_iPlayerSprintPoints[client] >= 100) - { - g_iPlayerSprintPoints[client] = 100; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - return; - } - - g_iPlayerSprintPoints[client]++; - ClientSprintTimer(client, true); -} - -// ========================================================== -// PROXY / GHOST AND GLOW FUNCTIONS -// ========================================================== - -ClientResetProxy(client, bool:bResetFull=true) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetProxy(%d)", client); -#endif - - new iOldMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - new String:sOldProfileName[SF2_MAX_PROFILE_NAME_LENGTH]; - if (iOldMaster >= 0) - { - NPCGetProfile(iOldMaster, sOldProfileName, sizeof(sOldProfileName)); - } - - new bool:bOldProxy = g_bPlayerProxy[client]; - if (bResetFull) - { - g_bPlayerProxy[client] = false; - g_iPlayerProxyMaster[client] = -1; - } - - g_iPlayerProxyControl[client] = 0; - g_hPlayerProxyControlTimer[client] = INVALID_HANDLE; - g_flPlayerProxyControlRate[client] = 0.0; - g_flPlayerProxyVoiceTimer[client] = INVALID_HANDLE; - - if (IsClientInGame(client)) - { - if (bOldProxy) - { - ClientStartProxyAvailableTimer(client); - - if (bResetFull) - { - SetVariantString(""); - AcceptEntityInput(client, "SetCustomModel"); - } - - if (sOldProfileName[0]) - { - ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_spawn", GetProfileNum(sOldProfileName, "sound_proxy_spawn_channel", SNDCHAN_AUTO)); - ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_hurt", GetProfileNum(sOldProfileName, "sound_proxy_hurt_channel", SNDCHAN_AUTO)); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetProxy(%d)", client); -#endif -} - -ClientStartProxyAvailableTimer(client) -{ - g_bPlayerProxyAvailable[client] = false; - g_hPlayerProxyAvailableTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerProxyWaitTime), Timer_ClientProxyAvailable, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStartProxyForce(client, iSlenderID, const Float:flPos[3]) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); -#endif - - g_iPlayerProxyAskMaster[client] = iSlenderID; - for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; - - g_iPlayerProxyAvailableCount[client] = 0; - g_bPlayerProxyAvailableInForce[client] = true; - g_hPlayerProxyAvailableTimer[client] = CreateTimer(1.0, Timer_ClientForceProxy, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerProxyAvailableTimer[client], true); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); -#endif -} - -ClientStopProxyForce(client) -{ - g_iPlayerProxyAvailableCount[client] = 0; - g_bPlayerProxyAvailableInForce[client] = false; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; -} - -public Action:Timer_ClientForceProxy(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerProxyAvailableTimer[client]) return Plugin_Stop; - - if (!IsRoundEnding()) - { - new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[client]); - if (iBossIndex != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumProxies = 0; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; - - iNumProxies++; - } - - if (iNumProxies < iMaxProxies) - { - if (g_iPlayerProxyAvailableCount[client] > 0) - { - g_iPlayerProxyAvailableCount[client]--; - - SetHudTextParams(-1.0, 0.25, - 1.0, - 255, 255, 255, 255, - _, - _, - 0.25, 1.25); - - ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Proxy Force Message", client, g_iPlayerProxyAvailableCount[client]); - - return Plugin_Continue; - } - else - { - ClientEnableProxy(client, iBossIndex); - TeleportEntity(client, g_iPlayerProxyAskPosition[client], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - } - } - else - { - PrintToChat(client, "%T", "SF2 Too Many Proxies", client); - } - } - } - - ClientStopProxyForce(client); - return Plugin_Stop; -} - -DisplayProxyAskMenu(client, iAskMaster, const Float:flPos[3]) -{ - decl String:sBuffer[512]; - new Handle:hMenu = CreateMenu(Menu_ProxyAsk); - SetMenuTitle(hMenu, "%T\n \n%T\n \n", "SF2 Proxy Ask Menu Title", client, "SF2 Proxy Ask Menu Description", client); - - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, "1", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - - g_iPlayerProxyAskMaster[client] = iAskMaster; - for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; - DisplayMenu(hMenu, client, 15); -} - -public Menu_ProxyAsk(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_End: CloseHandle(menu); - case MenuAction_Select: - { - if (!IsRoundEnding()) - { - new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[param1]); - if (iBossIndex != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumProxies; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; - - iNumProxies++; - } - - if (iNumProxies < iMaxProxies) - { - if (param2 == 0) - { - ClientEnableProxy(param1, iBossIndex); - TeleportEntity(param1, g_iPlayerProxyAskPosition[param1], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - } - else - { - ClientStartProxyAvailableTimer(param1); - } - } - else - { - PrintToChat(param1, "%T", "SF2 Too Many Proxies", param1); - } - } - } - } - } -} - -public Action:Timer_ClientProxyAvailable(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerProxyAvailableTimer[client]) return; - - g_bPlayerProxyAvailable[client] = true; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; -} - -ClientEnableProxy(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) return; - if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) return; - if (g_bPlayerProxy[client]) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - PvP_SetPlayerPvPState(client, false, false, false); - - ClientSetGhostModeState(client, false); - - ClientStopProxyForce(client); - - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - if (!IsPlayerAlive(client)) TF2_RespawnPlayer(client); - // Speed recalculation. Props to the creators of FF2/VSH for this snippet. - TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); - - g_bPlayerProxy[client] = true; - g_iPlayerProxyMaster[client] = NPCGetUniqueID(iBossIndex); - g_iPlayerProxyControl[client] = 100; - g_flPlayerProxyControlRate[client] = GetProfileFloat(sProfile, "proxies_controldrainrate"); - g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - g_bPlayerProxyAvailable[client] = false; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; - - decl String:sAllowedClasses[512]; - GetProfileString(sProfile, "proxies_classes", sAllowedClasses, sizeof(sAllowedClasses)); - - decl String:sClassName[64]; - TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); - if (sAllowedClasses[0] && sClassName[0] && StrContains(sAllowedClasses, sClassName, false) == -1) - { - // Pick the first class that's allowed. - new String:sAllowedClassesList[32][32]; - new iClassCount = ExplodeString(sAllowedClasses, " ", sAllowedClassesList, 32, 32); - if (iClassCount) - { - TF2_SetPlayerClass(client, TF2_GetClass(sAllowedClassesList[0]), _, false); - - new iMaxHealth = GetEntProp(client, Prop_Send, "m_iHealth"); - TF2_RegeneratePlayer(client); - SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); - SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); - } - } - - UTIL_ScreenFade(client, 200, 1, FFADE_IN, 255, 255, 255, 100); - PrecacheSound("weapons/teleporter_send.wav"); - EmitSoundToClient(client, "weapons/teleporter_send.wav", _, SNDCHAN_STATIC); - - ClientActivateUltravision(client); - - CreateTimer(0.33, Timer_ApplyCustomModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - Call_StartForward(fOnClientSpawnedAsProxy); - Call_PushCell(client); - Call_Finish(); -} - -public Action:Timer_ClientProxyControl(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerProxyControlTimer[client]) return; - - g_iPlayerProxyControl[client]--; - if (g_iPlayerProxyControl[client] <= 0) - { - // ForcePlayerSuicide isn't really dependable, since the player doesn't suicide until several seconds after spawning has passed. - SDKHooks_TakeDamage(client, client, client, 9001.0, DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - return; - } - - g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, userid, TIMER_FLAG_NO_MAPCHANGE); -} - -bool:DoesClientHaveConstantGlow(client) -{ - return g_bPlayerConstantGlowEnabled[client]; -} - -ClientDisableConstantGlow(client) -{ - if (!DoesClientHaveConstantGlow(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientDisableConstantGlow(%d)", client); -#endif - - g_bPlayerConstantGlowEnabled[client] = false; - - new iGlow = EntRefToEntIndex(g_iPlayerConstantGlowEntity[client]); - if (iGlow && iGlow != INVALID_ENT_REFERENCE) AcceptEntityInput(iGlow, "Kill"); - - g_iPlayerConstantGlowEntity[client] = INVALID_ENT_REFERENCE; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientDisableConstantGlow(%d)", client); -#endif -} - -bool:ClientEnableConstantGlow(client, const String:sAttachment[]="") -{ - if (DoesClientHaveConstantGlow(client)) return true; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientEnableConstantGlow(%d)", client); -#endif - - decl String:sModel[PLATFORM_MAX_PATH]; - GetClientModel(client, sModel, sizeof(sModel)); - - if (strlen(sModel) == 0) - { - // For some reason the model couldn't be found, so no. - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false (no model specified)", client); -#endif - - return false; - } - - new iGlow = CreateEntityByName("tf_taunt_prop"); - if (iGlow != -1) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> created"); -#endif - - g_bPlayerConstantGlowEnabled[client] = true; - g_iPlayerConstantGlowEntity[client] = EntIndexToEntRef(iGlow); - - new Float:flModelScale = GetEntPropFloat(client, Prop_Send, "m_flModelScale"); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) - { - DebugMessage("tf_taunt_prop -> get model and model scale (%s, %f, player class: %d)", sModel, flModelScale, TF2_GetPlayerClass(client)); - } -#endif - - SetEntityModel(iGlow, sModel); - DispatchSpawn(iGlow); - ActivateEntity(iGlow); - SetEntityRenderMode(iGlow, RENDER_TRANSCOLOR); - SetEntityRenderColor(iGlow, 0, 0, 0, 0); - SetEntProp(iGlow, Prop_Send, "m_bGlowEnabled", 1); - SetEntPropFloat(iGlow, Prop_Send, "m_flModelScale", flModelScale); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set model and model scale"); -#endif - - // Set effect flags. - new iFlags = GetEntProp(iGlow, Prop_Send, "m_fEffects"); - SetEntProp(iGlow, Prop_Send, "m_fEffects", iFlags | (1 << 0)); // EF_BONEMERGE - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set bonemerge flags"); -#endif - - SetVariantString("!activator"); - AcceptEntityInput(iGlow, "SetParent", client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent to client"); -#endif - - if (sAttachment[0]) - { - SetVariantString(sAttachment); - AcceptEntityInput(iGlow, "SetParentAttachment"); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent attachment to %s", sAttachment); -#endif - - SDKHook(iGlow, SDKHook_SetTransmit, Hook_ConstantGlowSetTransmit); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> true", client); -#endif - - return true; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false", client); -#endif - - return false; -} - -ClientResetJumpScare(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetJumpScare(%d)", client); -#endif - - g_iPlayerJumpScareBoss[client] = -1; - g_flPlayerJumpScareLifeTime[client] = -1.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetJumpScare(%d)", client); -#endif -} - -ClientDoJumpScare(client, iBossIndex, Float:flLifeTime) -{ - g_iPlayerJumpScareBoss[client] = NPCGetUniqueID(iBossIndex); - g_flPlayerJumpScareLifeTime[client] = GetGameTime() + flLifeTime; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_jumpscare", sBuffer, sizeof(sBuffer), 1); - - if (strlen(sBuffer) > 0) - { - EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN); - } -} - - /** - * Handles sprinting upon player input. - */ -ClientHandleSprint(client, bool:bSprint) -{ - if (!IsPlayerAlive(client) || - g_bPlayerEliminated[client] || - DidClientEscape(client) || - g_bPlayerProxy[client] || - IsClientInGhostMode(client)) return; - - if (bSprint) - { - if (g_iPlayerSprintPoints[client] > 0) - { - ClientStartSprint(client); - } - else - { - EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); - } - } - else - { - if (IsClientSprinting(client)) - { - ClientStopSprint(client); - } - } -} - -ClientOnButtonPress(client, button) -{ - switch (button) - { - case IN_ATTACK2: - { - if (IsPlayerAlive(client)) - { - if (!IsRoundInWarmup() && - !IsRoundInIntro() && - !IsRoundEnding() && - !DidClientEscape(client)) - { - if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) - { - ClientHandleFlashlight(client); - } - } - } - } - case IN_ATTACK3: - { - ClientHandleSprint(client, true); - if(IsClientInGhostMode(client) && g_iGhostNextHelpPhrase[client] < GetTime()) { - EmitSoundToAll(g_strGhostHelpPhrases[GetRandomInt(0, sizeof(g_strGhostHelpPhrases) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - g_iGhostNextHelpPhrase[client] = GetTime() + g_iGhostHelpPhraseInterval; - } - } - case IN_RELOAD: - { - if (IsPlayerAlive(client)) - { - if (!g_bPlayerEliminated[client]) - { - if (!IsRoundEnding() && - !IsRoundInWarmup() && - !IsRoundInIntro() && - !DidClientEscape(client)) - { - ClientBlink(client); - } - } - } - } - case IN_JUMP: - { - if (IsPlayerAlive(client)) - { - if (!bool:GetEntProp(client, Prop_Send, "m_bDucked") && - (GetEntityFlags(client) & FL_ONGROUND) && - GetEntProp(client, Prop_Send, "m_nWaterLevel") < 2) - { - ClientOnJump(client); - } - } - } - } -} - -ClientOnButtonRelease(client, button) -{ - switch (button) - { - case IN_ATTACK3: - { - ClientHandleSprint(client, false); - } - } -} - -ClientOnJump(client) -{ - if (!g_bPlayerEliminated[client]) - { - if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); - if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) - { - g_iPlayerSprintPoints[client] -= 7; - if (g_iPlayerSprintPoints[client] < 0) g_iPlayerSprintPoints[client] = 0; - } - - if (!IsClientSprinting(client)) - { - if (g_hPlayerSprintTimer[client] == INVALID_HANDLE) - { - // If the player hasn't sprinted recently, force us to regenerate the stamina. - ClientSprintTimer(client, true); - } - } - } - } -} - -// ========================================================== -// DEATH CAM FUNCTIONS -// ========================================================== - -bool:IsClientInDeathCam(client) -{ - return g_bPlayerDeathCam[client]; -} - -public Action:Hook_DeathCamSetTransmit(slender, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerDeathCamEnt2[other]) != slender) return Plugin_Handled; - return Plugin_Continue; -} - -ClientResetDeathCam(client) -{ - if (!IsClientInDeathCam(client)) return; // no really need to reset if it wasn't set. - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetDeathCam(%d)", client); -#endif - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - - g_iPlayerDeathCamBoss[client] = -1; - g_bPlayerDeathCam[client] = false; - g_bPlayerDeathCamShowOverlay[client] = false; - g_hPlayerDeathCamTimer[client] = INVALID_HANDLE; - - new ent = EntRefToEntIndex(g_iPlayerDeathCamEnt[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Disable"); - AcceptEntityInput(ent, "Kill"); - } - - ent = EntRefToEntIndex(g_iPlayerDeathCamEnt2[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerDeathCamEnt[client] = INVALID_ENT_REFERENCE; - g_iPlayerDeathCamEnt2[client] = INVALID_ENT_REFERENCE; - - if (IsClientInGame(client)) - { - SetClientViewEntity(client, client); - } - - Call_StartForward(fOnClientEndDeathCam); - Call_PushCell(client); - Call_PushCell(iDeathCamBoss); - Call_Finish(); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetDeathCam(%d)", client); -#endif -} - -ClientStartDeathCam(client, iBossIndex, const Float:vecLookPos[3]) -{ - if (IsClientInDeathCam(client)) return; - if (!NPCIsValid(iBossIndex)) return; - - decl String:buffer[PLATFORM_MAX_PATH]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (GetProfileNum(sProfile, "death_cam_play_scare_sound")) - { - GetRandomStringFromProfile(sProfile, "sound_scare_player", buffer, sizeof(buffer)); - if (buffer[0]) EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - - GetRandomStringFromProfile(sProfile, "sound_player_deathcam", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - else - { - // Legacy support for "sound_player_death" - GetRandomStringFromProfile(sProfile, "sound_player_death", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - - GetRandomStringFromProfile(sProfile, "sound_player_deathcam_all", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - else - { - // Legacy support for "sound_player_death_all" - GetRandomStringFromProfile(sProfile, "sound_player_death_all", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - - // Call our forward. - Call_StartForward(fOnClientCaughtByBoss); - Call_PushCell(client); - Call_PushCell(iBossIndex); - Call_Finish(); - - if (!NPCHasDeathCamEnabled(iBossIndex)) - { - SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol changes our lifestate. - - // TODO: Add more attributes! - if (NPCHasAttribute(iBossIndex, "ignite player on death")) - { - new Float:flValue = NPCGetAttributeValue(iBossIndex, "ignite player on death"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - - SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - return; - } - - g_iPlayerDeathCamBoss[client] = NPCGetUniqueID(iBossIndex); - g_bPlayerDeathCam[client] = true; - g_bPlayerDeathCamShowOverlay[client] = false; - - decl Float:eyePos[3], Float:eyeAng[3], Float:vecAng[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - SubtractVectors(eyePos, vecLookPos, vecAng); - GetVectorAngles(vecAng, vecAng); - vecAng[0] = 0.0; - vecAng[2] = 0.0; - - // Create fake model. - new slender = SpawnSlenderModel(iBossIndex, vecLookPos); - TeleportEntity(slender, vecLookPos, vecAng, NULL_VECTOR); - g_iPlayerDeathCamEnt2[client] = EntIndexToEntRef(slender); - SDKHook(slender, SDKHook_SetTransmit, Hook_DeathCamSetTransmit); - - // Create camera look point. - decl String:sName[64]; - Format(sName, sizeof(sName), "sf2_boss_%d", EntIndexToEntRef(slender)); - - decl Float:flOffsetPos[3]; - new target = CreateEntityByName("info_target"); - GetProfileVector(sProfile, "death_cam_pos", flOffsetPos); - AddVectors(vecLookPos, flOffsetPos, flOffsetPos); - TeleportEntity(target, flOffsetPos, NULL_VECTOR, NULL_VECTOR); - DispatchKeyValue(target, "targetname", sName); - SetVariantString("!activator"); - AcceptEntityInput(target, "SetParent", slender); - - // Create the camera itself. - new camera = CreateEntityByName("point_viewcontrol"); - TeleportEntity(camera, eyePos, eyeAng, NULL_VECTOR); - DispatchKeyValue(camera, "spawnflags", "12"); - DispatchKeyValue(camera, "target", sName); - DispatchSpawn(camera); - AcceptEntityInput(camera, "Enable", client); - g_iPlayerDeathCamEnt[client] = EntIndexToEntRef(camera); - - if (GetProfileNum(sProfile, "death_cam_overlay") && GetProfileFloat(sProfile, "death_cam_time_overlay_start") >= 0.0) - { - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_overlay_start"), Timer_ClientResetDeathCam1, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - - TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - - Call_StartForward(fOnClientStartDeathCam); - Call_PushCell(client); - Call_PushCell(iBossIndex); - Call_Finish(); -} - -public Action:Timer_ClientResetDeathCam1(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerDeathCamTimer[client]) return; - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); - - g_bPlayerDeathCamShowOverlay[client] = true; - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, userid, TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ClientResetDeathCamEnd(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerDeathCamTimer[client]) return; - - SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol entity changes our damage state. - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - if (iDeathCamBoss != -1) - { - if (NPCHasAttribute(iDeathCamBoss, "ignite player on death")) - { - new Float:flValue = NPCGetAttributeValue(iDeathCamBoss, "ignite player on death"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - } - - SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - ClientResetDeathCam(client); -} - -// ========================================================== -// GHOST MODE FUNCTIONS -// ========================================================== - -static bool:g_bPlayerGhostMode[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerGhostModeTarget[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Handle:g_hPlayerGhostModeConnectionCheckTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerGhostModeConnectionTimeOutTime[MAXPLAYERS + 1] = { -1.0, ... }; -static Float:g_flPlayerGhostModeConnectionBootTime[MAXPLAYERS + 1] = { -1.0, ... }; - -/** - * Enables/Disables ghost mode on the player. - */ -ClientSetGhostModeState(client, bool:bState) -{ - if (bState == g_bPlayerGhostMode[client]) return; - - if (bState && !IsClientInGame(client)) return; - - g_bPlayerGhostMode[client] = bState; - g_iPlayerGhostModeTarget[client] = INVALID_ENT_REFERENCE; - - if (bState) - { - ClientHandleGhostMode(client, true); - - if (GetConVarBool(g_cvGhostModeConnectionCheck)) - { - g_hPlayerGhostModeConnectionCheckTimer[client] = CreateTimer(0.0, Timer_GhostModeConnectionCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - } - - PvP_OnClientGhostModeEnable(client); - } - else - { - DestroySpriteOverlay(client); - g_hPlayerGhostModeConnectionCheckTimer[client] = INVALID_HANDLE; - g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - - if (IsClientInGame(client)) - { - TF2_RemoveCondition(client, TFCond_HalloweenGhostMode); - SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_PLAYER); - } - } -} - -public Action:Timer_GhostModeConnectionCheck(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerGhostModeConnectionCheckTimer[client]) return Plugin_Stop; - - if (!IsFakeClient(client) && IsClientTimingOut(client)) - { - new Float:bootTime = g_flPlayerGhostModeConnectionBootTime[client]; - if (bootTime < 0.0) - { - bootTime = GetGameTime() + GetConVarFloat(g_cvGhostModeConnectionTolerance); - g_flPlayerGhostModeConnectionBootTime[client] = bootTime; - g_flPlayerGhostModeConnectionTimeOutTime[client] = GetGameTime(); - } - - if (GetGameTime() >= bootTime) - { - ChangeClientTeamNoSuicide(client, _:TFTeam_Spectator); - /* - ClientSetGhostModeState(client, false); - TF2_RespawnPlayer(client); - */ - decl String:authString[128]; - GetClientAuthString(client, authString, sizeof(authString)); - - LogSF2Message("Removed %N (%s) from ghost mode due to timing out for %f seconds", client, authString, GetConVarFloat(g_cvGhostModeConnectionTolerance)); - - new Float:timeOutTime = g_flPlayerGhostModeConnectionTimeOutTime[client]; - CPrintToChat(client, "%T", "SF2 Ghost Mode Bad Connection", client, RoundFloat(bootTime - timeOutTime)); - - return Plugin_Stop; - } - } - else - { - // Player regained connection; reset. - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - } - - return Plugin_Continue; -} - -/** - * Makes sure that the player is a ghost when ghost mode is activated. - */ -ClientHandleGhostMode(client, bool:bForceSpawn=false) -{ - if (!IsClientInGhostMode(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientHandleGhostMode(%d, %d)", client, bForceSpawn); -#endif - - if (!TF2_IsPlayerInCondition(client, TFCond_HalloweenGhostMode) || bForceSpawn) - { - TF2_AddCondition(client, TFCond_HalloweenGhostMode, -1.0); - SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_DEBRIS); - - // Set first observer target. - ClientGhostModeNextTarget(client); - ClientActivateUltravision(client); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientHandleGhostMode(%d, %d)", client, bForceSpawn); -#endif -} - -ClientGhostModeNextTarget(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientGhostModeNextTarget(%d)", client); -#endif - if(GetActivePlayerCount() < 1) return; - - new iLastTarget = EntRefToEntIndex(g_iPlayerGhostModeTarget[client]); - new iNextTarget = -1; - new iFirstTarget = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientInGame(i) && (!g_bPlayerEliminated[i] || g_bPlayerProxy[i]) && !IsClientInGhostMode(i) && !DidClientEscape(i) && IsPlayerAlive(i)) - { - if (iFirstTarget == -1) iFirstTarget = i; - if (i > iLastTarget) - { - iNextTarget = i; - break; - } - } - } - - new iTarget = -1; - if (IsValidClient(iNextTarget)) iTarget = iNextTarget; - else iTarget = iFirstTarget; - - if (IsValidClient(iTarget)) - { - g_iPlayerGhostModeTarget[client] = EntIndexToEntRef(iTarget); - - decl Float:flPos[3], Float:flAng[3], Float:flVelocity[3]; - GetClientAbsOrigin(iTarget, flPos); - GetClientEyeAngles(iTarget, flAng); - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsVelocity", flVelocity); - TeleportEntity(client, flPos, flAng, flVelocity); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientGhostModeNextTarget(%d)", client); -#endif -} - -bool:IsClientInGhostMode(client) -{ - return g_bPlayerGhostMode[client]; -} - -// ========================================================== -// SCARE FUNCTIONS -// ========================================================== - -ClientPerformScare(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) - { - LogError("Could not perform scare on client %d: boss does not exist!", client); - return; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - g_flPlayerScareLastTime[client][iBossIndex] = GetGameTime(); - g_flPlayerScareNextTime[client][iBossIndex] = GetGameTime() + NPCGetScareCooldown(iBossIndex); - - // See how much Sanity should be drained from a scare. - new Float:flStaticAmount = GetProfileFloat(sProfile, "scare_static_amount", 0.0); - g_flPlayerStaticAmount[client] += flStaticAmount; - if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; - - decl String:sScareSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_scare_player", sScareSound, sizeof(sScareSound)); - - if (sScareSound[0]) - { - EmitSoundToClient(client, sScareSound, _, MUSIC_CHAN, SNDLEVEL_NONE); - - if (NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS) - { - new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); - new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); - - g_flPlayerSightSoundNextTime[client][iBossIndex] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); - } - - if (g_flPlayerStress[client] > 0.4) - { - ClientAddStress(client, 0.4); - } - else - { - ClientAddStress(client, 0.66); - } - } - else - { - if (g_flPlayerStress[client] > 0.4) - { - ClientAddStress(client, 0.3); - } - else - { - ClientAddStress(client, 0.45); - } - } -} - -ClientPerformSightSound(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) - { - LogError("Could not perform sight sound on client %d: boss does not exist!", client); - return; - } - - if (!(NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS)) return; - - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); - if (iMaster == -1) iMaster = iBossIndex; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sSightSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_sight", sSightSound, sizeof(sSightSound)); - - if (sSightSound[0]) - { - EmitSoundToClient(client, sSightSound, _, MUSIC_CHAN, SNDLEVEL_NONE); - - new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); - new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); - - g_flPlayerSightSoundNextTime[client][iMaster] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); - - decl Float:flBossPos[3], Float:flMyPos[3]; - new iBoss = NPCGetEntIndex(iBossIndex); - GetClientAbsOrigin(client, flMyPos); - GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flBossPos); - new Float:flDistUnComfortZone = 400.0; - new Float:flBossDist = GetVectorDistance(flMyPos, flBossPos); - - new Float:flStressScalar = 1.0 + (flDistUnComfortZone / flBossDist); - - ClientAddStress(client, 0.1 * flStressScalar); - } - else - { - LogError("Warning! %s supports sight sounds, but was given a blank sound!", sProfile); - } -} - -ClientResetScare(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetScare(%d)", client); -#endif - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_flPlayerScareNextTime[client][i] = -1.0; - g_flPlayerScareLastTime[client][i] = -1.0; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetScare(%d)", client); -#endif -} - -// ========================================================== -// ANTI-CAMPING FUNCTIONS -// ========================================================== - -stock ClientResetCampingStats(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetCampingStats(%d)", client); -#endif - - g_iPlayerCampingStrikes[client] = 0; - g_hPlayerCampingTimer[client] = INVALID_HANDLE; - g_bPlayerCampingFirstTime[client] = true; - g_flPlayerCampingLastPosition[client][0] = 0.0; - g_flPlayerCampingLastPosition[client][1] = 0.0; - g_flPlayerCampingLastPosition[client][2] = 0.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetCampingStats(%d)", client); -#endif -} - -ClientStartCampingTimer(client) -{ - g_hPlayerCampingTimer[client] = CreateTimer(5.0, Timer_ClientCheckCamp, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ClientCheckCamp(Handle:timer, any:userid) -{ - if (IsRoundInWarmup()) return Plugin_Stop; - - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerCampingTimer[client]) return Plugin_Stop; - - if (IsRoundEnding() || !IsPlayerAlive(client) || g_bPlayerEliminated[client] || DidClientEscape(client)) return Plugin_Stop; - - if (!g_bPlayerCampingFirstTime[client]) - { - decl Float:flPos[3], Float:flMaxs[3], Float:flMins[3]; - GetClientAbsOrigin(client, flPos); - GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); - GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); - - // Only do something if the player is NOT stuck. - new Float:flDistFromLastPosition = GetVectorDistance(g_flPlayerCampingLastPosition[client], flPos); - new Float:flDistFromClosestBoss = 9999999.0; - new iClosestBoss = -1; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new iSlender = NPCGetEntIndex(i); - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; - - decl Float:flSlenderPos[3]; - SlenderGetAbsOrigin(i, flSlenderPos); - - new Float:flDist = GetVectorDistance(flSlenderPos, flPos); - if (flDist < flDistFromClosestBoss) - { - iClosestBoss = i; - flDistFromClosestBoss = flDist; - } - } - - if (GetConVarBool(g_cvCampingEnabled) && - !g_bRoundGrace && - !IsSpaceOccupiedIgnorePlayers(flPos, flMins, flMaxs, client) && - g_flPlayerStaticAmount[client] <= GetConVarFloat(g_cvCampingNoStrikeSanity) && - (iClosestBoss == -1 || flDistFromClosestBoss >= GetConVarFloat(g_cvCampingNoStrikeBossDistance)) && - flDistFromLastPosition <= GetConVarFloat(g_cvCampingMinDistance)) - { - g_iPlayerCampingStrikes[client]++; - if (g_iPlayerCampingStrikes[client] < GetConVarInt(g_cvCampingMaxStrikes)) - { - if (g_iPlayerCampingStrikes[client] >= GetConVarInt(g_cvCampingStrikesWarn)) - { - CPrintToChat(client, "{red}%T", "SF2 Camping System Warning", client, (GetConVarInt(g_cvCampingMaxStrikes) - g_iPlayerCampingStrikes[client]) * 5); - } - } - else - { - g_iPlayerCampingStrikes[client] = 0; - ClientStartDeathCam(client, 0, flPos); - } - } - else - { - // Forgiveness. - if (g_iPlayerCampingStrikes[client] > 0) g_iPlayerCampingStrikes[client]--; - } - - g_flPlayerCampingLastPosition[client][0] = flPos[0]; - g_flPlayerCampingLastPosition[client][1] = flPos[1]; - g_flPlayerCampingLastPosition[client][2] = flPos[2]; - } - else - { - g_bPlayerCampingFirstTime[client] = false; - } - - return Plugin_Continue; -} - -// ========================================================== -// BLINK FUNCTIONS -// ========================================================== - -bool:IsClientBlinking(client) -{ - return g_bPlayerBlink[client]; -} - -Float:ClientGetBlinkMeter(client) -{ - return g_flPlayerBlinkMeter[client]; -} - -ClientGetBlinkCount(client) -{ - return g_iPlayerBlinkCount[client]; -} - -/** - * Resets all data on blinking. - */ -ClientResetBlink(client) -{ - g_hPlayerBlinkTimer[client] = INVALID_HANDLE; - g_bPlayerBlink[client] = false; - g_flPlayerBlinkMeter[client] = 1.0; - g_iPlayerBlinkCount[client] = 0; -} - -/** - * Sets the player into a blinking state and blinds the player - */ -ClientBlink(client) -{ - if (IsRoundInWarmup() || DidClientEscape(client)) return; - - if (IsClientBlinking(client)) return; - - g_bPlayerBlink[client] = true; - g_iPlayerBlinkCount[client]++; - g_flPlayerBlinkMeter[client] = 0.0; - g_hPlayerBlinkTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerBlinkHoldTime), Timer_BlinkTimer2, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - UTIL_ScreenFade(client, 100, RoundToFloor(GetConVarFloat(g_cvPlayerBlinkHoldTime) * 1000.0), FFADE_IN, 0, 0, 0, 255); - - Call_StartForward(fOnClientBlink); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Unsets the player from the blinking state. - */ -ClientUnblink(client) -{ - if (!IsClientBlinking(client)) return; - - g_bPlayerBlink[client] = false; - g_hPlayerBlinkTimer[client] = INVALID_HANDLE; - g_flPlayerBlinkMeter[client] = 1.0; -} - -ClientStartDrainingBlinkMeter(client) -{ - g_hPlayerBlinkTimer[client] = CreateTimer(ClientGetBlinkRate(client), Timer_BlinkTimer, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_BlinkTimer(Handle:timer, any:userid) -{ - if (IsRoundInWarmup()) return Plugin_Stop; - - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerBlinkTimer[client]) return Plugin_Stop; - - if (IsPlayerAlive(client) && !IsClientInDeathCam(client) && !g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !IsRoundEnding()) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteBlinkOverride); - if ((!g_bRoundInfiniteBlink && iOverride != 1) || iOverride == 0) - { - g_flPlayerBlinkMeter[client] -= 0.05; - } - - if (g_flPlayerBlinkMeter[client] <= 0.0) - { - ClientBlink(client); - return Plugin_Stop; - } - } - - return Plugin_Continue; -} - -public Action:Timer_BlinkTimer2(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerBlinkTimer[client]) return; - - ClientUnblink(client); - ClientStartDrainingBlinkMeter(client); -} - -Float:ClientGetBlinkRate(client) -{ - new Float:flValue = GetConVarFloat(g_cvPlayerBlinkRate); - if (GetEntProp(client, Prop_Send, "m_nWaterLevel") >= 3) - { - // Being underwater makes you blink faster, obviously. - flValue *= 0.75; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - if (g_bPlayerSeesSlender[client][i]) - { - flValue *= GetProfileFloat(sProfile, "blink_look_rate_multiply", 1.0); - } - - else if (g_iPlayerStaticMode[client][i] == Static_Increase) - { - flValue *= GetProfileFloat(sProfile, "blink_static_rate_multiply", 1.0); - } - } - - if (TF2_GetPlayerClass(client) == TFClass_Sniper) flValue *= 1.4; - - if (IsClientUsingFlashlight(client)) - { - decl Float:startPos[3], Float:endPos[3], Float:flDirection[3]; - new Float:flLength = SF2_FLASHLIGHT_LENGTH; - GetClientEyePosition(client, startPos); - GetClientEyePosition(client, endPos); - GetClientEyeAngles(client, flDirection); - GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flDirection, flDirection); - ScaleVector(flDirection, flLength); - AddVectors(endPos, flDirection, endPos); - new Handle:hTrace = TR_TraceRayFilterEx(startPos, endPos, MASK_VISIBLE, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); - TR_GetEndPosition(endPos, hTrace); - new bool:bHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bHit) - { - new Float:flPercent = (GetVectorDistance(startPos, endPos) / flLength); - flPercent *= 3.5; - if (flPercent > 1.0) flPercent = 1.0; - flValue *= flPercent; - } - } - - return flValue; -} - -// ========================================================== -// SCREEN OVERLAY FUNCTIONS -// ========================================================== - -ClientAddStress(client, Float:flStressAmount) -{ - g_flPlayerStress[client] += flStressAmount; - if (g_flPlayerStress[client] < 0.0) g_flPlayerStress[client] = 0.0; - if (g_flPlayerStress[client] > 1.0) g_flPlayerStress[client] = 1.0; - - //PrintCenterText(client, "g_flPlayerStress[%d] = %f", client, g_flPlayerStress[client]); - - SlenderOnClientStressUpdate(client); -} - -stock ClientResetOverlay(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetOverlay(%d)", client); -#endif - - g_hPlayerOverlayCheck[client] = INVALID_HANDLE; - - if (IsClientInGame(client)) - { - ClientCommand(client, "r_screenoverlay \"\""); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetOverlay(%d)", client); -#endif -} - -public DestroySpriteOverlay(client) -{ - if(IsValidClient(client) && client > 0) { - for(new i = 0; i < sizeof(g_iOverlayRef[]); i++) { - Overlay_Render_Clear_Layer(client, i); - } - g_hOverlayUpdateTimer[client] = INVALID_HANDLE; - //Show_Weapon(client); - } -} - -public Action:Timer_PlayerOverlayCheck(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerOverlayCheck[client]) return Plugin_Stop; - - if (IsRoundInWarmup()) return Plugin_Continue; - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - new iJumpScareBoss = NPCGetFromUniqueID(g_iPlayerJumpScareBoss[client]); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sMaterial[PLATFORM_MAX_PATH]; - - if (IsClientInDeathCam(client) && iDeathCamBoss != -1 && g_bPlayerDeathCamShowOverlay[client]) - { - DestroySpriteOverlay(client); - NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); - GetRandomStringFromProfile(sProfile, "overlay_player_death", sMaterial, sizeof(sMaterial), 1); - } - else if (iJumpScareBoss != -1 && GetGameTime() <= g_flPlayerJumpScareLifeTime[client]) - { - DestroySpriteOverlay(client); - NPCGetProfile(iJumpScareBoss, sProfile, sizeof(sProfile)); - GetRandomStringFromProfile(sProfile, "overlay_jumpscare", sMaterial, sizeof(sMaterial), 1); - } - else if (IsRoundInWarmup() || g_bPlayerEliminated[client] || DidClientEscape(client) && !IsClientInGhostMode(client)) - { - DestroySpriteOverlay(client); - return Plugin_Continue; - } - else - { - strcopy(sMaterial, sizeof(sMaterial), GRAIN_OVERLAY); - } - - ClientCommand(client, "r_screenoverlay %s", sMaterial); - return Plugin_Continue; -} - -public Hide_Weapon(client) { - if(!IsValidClient(client) || client < 1) return; - new v_model = GetEntPropEnt(client, Prop_Send, "m_hViewModel"); - if(v_model < 1) return; - //SetEntProp(v_model, Prop_Send, "m_nModelIndex", -1); - new EntEffects = GetEntProp(v_model, Prop_Send, "m_fEffects"); - EntEffects |= EF_NODRAW; - SetEntProp(v_model, Prop_Send, "m_fEffects", EntEffects); -} - -public Show_Weapon(client) { - if(!IsValidClient(client) || client < 1) return; - new v_model = GetEntPropEnt(client, Prop_Send, "m_hViewModel"); - if(v_model < 1) return; - //SetEntProp(v_model, Prop_Send, "m_nModelIndex", -1); - new EntEffects = GetEntProp(v_model, Prop_Send, "m_fEffects"); - EntEffects &= ~EF_NODRAW; - SetEntProp(v_model, Prop_Send, "m_fEffects", EntEffects); -} - -// ========================================================== -// MUSIC SYSTEM FUNCTIONS -// ========================================================== - -stock ClientUpdateMusicSystem(client, bool:bInitialize=false) -{ - new iOldPageMusicMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); - new iOldMusicFlags = g_iPlayerMusicFlags[client]; - new iChasingBoss = -1; - new iChasingSeeBoss = -1; - new iAlertBoss = -1; - new i20DollarsBoss = -1; - - if (IsRoundEnding() || !IsClientInGame(client) || IsFakeClient(client) || DidClientEscape(client) || (g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !g_bPlayerProxy[client])) - { - g_iPlayerMusicFlags[client] = 0; - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - } - else - { - new bool:bPlayMusicOnEscape = true; - decl String:sName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_escape_custommusic", false)) - { - bPlayMusicOnEscape = false; - break; - } - } - - // Page music first. - new iPageRange = 0; - - if (GetArraySize(g_hPageMusicRanges) > 0) // Map has its own defined page music? - { - for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) - { - ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - new iMin = GetArrayCell(g_hPageMusicRanges, i, 1); - new iMax = GetArrayCell(g_hPageMusicRanges, i, 2); - - if (g_iPageCount >= iMin && g_iPageCount <= iMax) - { - g_iPlayerPageMusicMaster[client] = GetArrayCell(g_hPageMusicRanges, i); - break; - } - } - } - else // Nope. Use old system instead. - { - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - - new Float:flPercent = g_iPageMax > 0 ? (float(g_iPageCount) / float(g_iPageMax)) : 0.0; - if (flPercent > 0.0 && flPercent <= 0.25) iPageRange = 1; - else if (flPercent > 0.25 && flPercent <= 0.5) iPageRange = 2; - else if (flPercent > 0.5 && flPercent <= 0.75) iPageRange = 3; - else if (flPercent > 0.75) iPageRange = 4; - - if (iPageRange == 1) ClientAddMusicFlag(client, MUSICF_PAGES1PERCENT); - else if (iPageRange == 2) ClientAddMusicFlag(client, MUSICF_PAGES25PERCENT); - else if (iPageRange == 3) ClientAddMusicFlag(client, MUSICF_PAGES50PERCENT); - else if (iPageRange == 4) ClientAddMusicFlag(client, MUSICF_PAGES75PERCENT); - } - - if (iPageRange != 1) ClientRemoveMusicFlag(client, MUSICF_PAGES1PERCENT); - if (iPageRange != 2) ClientRemoveMusicFlag(client, MUSICF_PAGES25PERCENT); - if (iPageRange != 3) ClientRemoveMusicFlag(client, MUSICF_PAGES50PERCENT); - if (iPageRange != 4) ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); - - if (IsRoundInEscapeObjective() && !bPlayMusicOnEscape) - { - ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - } - - new iOldChasingBoss = g_iPlayerChaseMusicMaster[client]; - new iOldChasingSeeBoss = g_iPlayerChaseMusicSeeMaster[client]; - new iOldAlertBoss = g_iPlayerAlertMusicMaster[client]; - new iOld20DollarsBoss = g_iPlayer20DollarsMusicMaster[client]; - - new Float:flAnger = -1.0; - new Float:flSeeAnger = -1.0; - new Float:flAlertAnger = -1.0; - new Float:fl20DollarsAnger = -1.0; - - decl Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetEntIndex(i) == INVALID_ENT_REFERENCE) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - new iBossType = NPCGetType(i); - - switch (iBossType) - { - case SF2BossType_Chaser: - { - GetClientAbsOrigin(client, flBuffer); - SlenderGetAbsOrigin(i, flBuffer3); - - new iTarget = EntRefToEntIndex(g_iSlenderTarget[i]); - if (iTarget != -1) - { - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBuffer2); - - if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK || g_iSlenderState[i] == STATE_STUN) && - !(NPCGetFlags(i) & SFF_MARKEDASFAKE) && - (iTarget == client || GetVectorDistance(flBuffer, flBuffer2) <= 850.0 || GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0)) - { - decl String:sPath[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sPath, sizeof(sPath), 1); - if (sPath[0]) - { - if (NPCGetAnger(i) > flAnger) - { - flAnger = NPCGetAnger(i); - iChasingBoss = i; - } - } - - if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK) && - PlayerCanSeeSlender(client, i, false)) - { - if (iOldChasingSeeBoss == -1 || !PlayerCanSeeSlender(client, iOldChasingSeeBoss, false) || (NPCGetAnger(i) > flSeeAnger)) - { - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sPath, sizeof(sPath), 1); - - if (sPath[0]) - { - flSeeAnger = NPCGetAnger(i); - iChasingSeeBoss = i; - } - } - - if (g_b20Dollars) - { - if (iOld20DollarsBoss == -1 || !PlayerCanSeeSlender(client, iOld20DollarsBoss, false) || (NPCGetAnger(i) > fl20DollarsAnger)) - { - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sPath, sizeof(sPath), 1); - - if (sPath[0]) - { - fl20DollarsAnger = NPCGetAnger(i); - i20DollarsBoss = i; - } - } - } - } - } - } - - if (g_iSlenderState[i] == STATE_ALERT) - { - decl String:sPath[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sPath, sizeof(sPath), 1); - if (!sPath[0]) continue; - - if (!(NPCGetFlags(i) & SFF_MARKEDASFAKE)) - { - if (GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0) - { - if (NPCGetAnger(i) > flAlertAnger) - { - flAlertAnger = NPCGetAnger(i); - iAlertBoss = i; - } - } - } - } - } - } - } - - if (iChasingBoss != iOldChasingBoss) - { - if (iChasingBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_CHASE); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_CHASE); - } - } - - if (iChasingSeeBoss != iOldChasingSeeBoss) - { - if (iChasingSeeBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_CHASEVISIBLE); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_CHASEVISIBLE); - } - } - - if (iAlertBoss != iOldAlertBoss) - { - if (iAlertBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_ALERT); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_ALERT); - } - } - - if (i20DollarsBoss != iOld20DollarsBoss) - { - if (i20DollarsBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_20DOLLARS); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_20DOLLARS); - } - } - } - - if (IsValidClient(client)) - { - new bool:bWasChase = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASE); - new bool:bChase = ClientHasMusicFlag(client, MUSICF_CHASE); - new bool:bWasChaseSee = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASEVISIBLE); - new bool:bChaseSee = ClientHasMusicFlag(client, MUSICF_CHASEVISIBLE); - new bool:bAlert = ClientHasMusicFlag(client, MUSICF_ALERT); - new bool:bWasAlert = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_ALERT); - new bool:b20Dollars = ClientHasMusicFlag(client, MUSICF_20DOLLARS); - new bool:bWas20Dollars = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_20DOLLARS); - - // Custom system. - if (GetArraySize(g_hPageMusicRanges) > 0) - { - decl String:sPath[PLATFORM_MAX_PATH]; - - new iMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); - if (iMaster != INVALID_ENT_REFERENCE) - { - for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) - { - new ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - - if (ent == iMaster && - (iOldPageMusicMaster != iMaster || iOldPageMusicMaster == INVALID_ENT_REFERENCE)) - { - if (!sPath[0]) - { - LogError("Could not play music of page range %d-%d: no sound path specified!", GetArrayCell(g_hPageMusicRanges, i, 1), GetArrayCell(g_hPageMusicRanges, i, 2)); - } - else - { - ClientMusicStart(client, sPath, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) - { - GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - StopSound(client, MUSIC_CHAN, sPath); - } - } - } - } - } - else - { - if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) - { - GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - StopSound(client, MUSIC_CHAN, sPath); - } - } - } - } - - // Old system. - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES1_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES1_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES2_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES2_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES3_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES3_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES4_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES4_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - new iMainMusicState = 0; - - if (bAlert != bWasAlert || iAlertBoss != g_iPlayerAlertMusicMaster[client]) - { - if (bAlert && !bChase) - { - ClientAlertMusicStart(client, iAlertBoss); - if (!bWasAlert) iMainMusicState = -1; - } - else - { - ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); - if (!bChase && bWasAlert) iMainMusicState = 1; - } - } - - if (bChase != bWasChase || iChasingBoss != g_iPlayerChaseMusicMaster[client]) - { - if (bChase) - { - ClientMusicChaseStart(client, iChasingBoss); - - if (!bWasChase) - { - iMainMusicState = -1; - - if (bAlert) - { - ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); - } - } - } - else - { - ClientMusicChaseStop(client, g_iPlayerChaseMusicMaster[client]); - if (bWasChase) - { - if (bAlert) - { - ClientAlertMusicStart(client, iAlertBoss); - } - else - { - iMainMusicState = 1; - } - } - } - } - - if (bChaseSee != bWasChaseSee || iChasingSeeBoss != g_iPlayerChaseMusicSeeMaster[client]) - { - if (bChaseSee) - { - ClientMusicChaseSeeStart(client, iChasingSeeBoss); - } - else - { - ClientMusicChaseSeeStop(client, g_iPlayerChaseMusicSeeMaster[client]); - } - } - - if (b20Dollars != bWas20Dollars || i20DollarsBoss != g_iPlayer20DollarsMusicMaster[client]) - { - if (b20Dollars) - { - Client20DollarsMusicStart(client, i20DollarsBoss); - } - else - { - Client20DollarsMusicStop(client, g_iPlayer20DollarsMusicMaster[client]); - } - } - - if (iMainMusicState == 1) - { - ClientMusicStart(client, g_strPlayerMusic[client], _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - else if (iMainMusicState == -1) - { - ClientMusicStop(client); - } - - if (bChase || bAlert) - { - new iBossToUse = -1; - if (bChase) - { - iBossToUse = iChasingBoss; - } - else - { - iBossToUse = iAlertBoss; - } - - if (iBossToUse != -1) - { - // We got some alert/chase music going on! The player's excitement will no doubt go up! - // Excitement, though, really depends on how close the boss is in relation to the - // player. - - new Float:flBossDist = NPCGetDistanceFromEntity(iBossToUse, client); - new Float:flScalar = flBossDist / 700.0 - if (flScalar > 1.0) flScalar = 1.0; - new Float:flStressAdd = 0.1 * (1.0 - flScalar); - - ClientAddStress(client, flStressAdd); - } - } - } -} - -stock ClientMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); - strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerMusicFlags[client] = 0; - g_flPlayerMusicVolume[client] = 0.0; - g_flPlayerMusicTargetVolume[client] = 0.0; - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; -} - -stock ClientMusicStart(client, const String:sNewMusic[], Float:flVolume=-1.0, Float:flTargetVolume=-1.0, bool:bCopyOnly=false) -{ - if (!IsValidClient(client)) return; - if (!sNewMusic[0]) return; - - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); - - if (!StrEqual(sOldMusic, sNewMusic, false)) - { - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - - strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), sNewMusic); - if (flVolume >= 0.0) g_flPlayerMusicVolume[client] = flVolume; - if (flTargetVolume >= 0.0) g_flPlayerMusicTargetVolume[client] = flTargetVolume; - - if (!bCopyOnly) - { - g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeInMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerMusicTimer[client], true); - } - else - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - } -} - -stock ClientMusicStop(client) -{ - g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeOutMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerMusicTimer[client], true); -} - -stock Client20DollarsMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayer20DollarsMusic[client]); - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayer20DollarsMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayer20DollarsMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayer20DollarsMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock Client20DollarsMusicStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayer20DollarsMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayer20DollarsMusicMaster[client] = iBossIndex; - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), sBuffer); - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeIn20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientAlertMusicStop(client, iOldMaster); - } -} - -stock Client20DollarsMusicStop(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayer20DollarsMusicMaster[client]) - { - g_iPlayer20DollarsMusicMaster[client] = -1; - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); - } - - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOut20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); -} - -stock ClientAlertMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerAlertMusic[client]); - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerAlertMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerAlertMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayerAlertMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_alert_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientAlertMusicStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayerAlertMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayerAlertMusicMaster[client] = iBossIndex; - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), sBuffer); - g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientAlertMusicStop(client, iOldMaster); - } -} - -stock ClientAlertMusicStop(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerAlertMusicMaster[client]) - { - g_iPlayerAlertMusicMaster[client] = -1; - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); - } - - g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); -} - -stock ClientChaseMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusic[client]); - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerChaseMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerChaseMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayerChaseMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_chase_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientMusicChaseStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayerChaseMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayerChaseMusicMaster[client] = iBossIndex; - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), sBuffer); - g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientMusicChaseStop(client, iOldMaster); - } -} - -stock ClientMusicChaseStop(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerChaseMusicMaster[client]) - { - g_iPlayerChaseMusicMaster[client] = -1; - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); - } - - g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); -} - -stock ClientChaseMusicSeeReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusicSee[client]); - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); - if (IsClientInGame(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerChaseMusicSeeMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerChaseMusicSeeTimer[client][i] = INVALID_HANDLE; - g_flPlayerChaseMusicSeeVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsClientInGame(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientMusicChaseSeeStart(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - - new iOldMaster = g_iPlayerChaseMusicSeeMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); - if (!sBuffer[0]) return; - - g_iPlayerChaseMusicSeeMaster[client] = iBossIndex; - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), sBuffer); - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientMusicChaseSeeStop(client, iOldMaster); - } -} - -stock ClientMusicChaseSeeStop(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerChaseMusicSeeMaster[client]) - { - g_iPlayerChaseMusicSeeMaster[client] = -1; - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); - } - - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); -} - -public Action:Timer_PlayerFadeInMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; - - g_flPlayerMusicVolume[client] += 0.07; - if (g_flPlayerMusicVolume[client] > g_flPlayerMusicTargetVolume[client]) g_flPlayerMusicVolume[client] = g_flPlayerMusicTargetVolume[client]; - - if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); - - if (g_flPlayerMusicVolume[client] >= g_flPlayerMusicTargetVolume[client]) - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; - - g_flPlayerMusicVolume[client] -= 0.07; - if (g_flPlayerMusicVolume[client] < 0.0) g_flPlayerMusicVolume[client] = 0.0; - - if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); - - if (g_flPlayerMusicVolume[client] <= 0.0) - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeIn20DollarsMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayer20DollarsMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayer20DollarsMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] > 1.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayer20DollarsMusic[client][0]) EmitSoundToClient(client, g_strPlayer20DollarsMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); - - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOut20DollarsMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayer20DollarsMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayer20DollarsMusic[client], false)) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayer20DollarsMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] < 0.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); - - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInAlertMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerAlertMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerAlertMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerAlertMusic[client][0]) EmitSoundToClient(client, g_strPlayerAlertMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); - - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutAlertMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerAlertMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerAlertMusic[client], false)) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerAlertMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); - - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInChaseMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerChaseMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerChaseMusic[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInChaseMusicSee(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerChaseMusicSee[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusicSee[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutChaseMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerChaseMusic[client], false)) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerChaseMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutChaseMusicSee(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerChaseMusicSee[client], false)) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -stock bool:ClientHasMusicFlag(client, iFlag) -{ - return bool:(g_iPlayerMusicFlags[client] & iFlag); -} - -stock bool:ClientHasMusicFlag2(iValue, iFlag) -{ - return bool:(iValue & iFlag); -} - -stock ClientAddMusicFlag(client, iFlag) -{ - if (!ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] |= iFlag; -} - -stock ClientRemoveMusicFlag(client, iFlag) -{ - if (ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] &= ~iFlag; -} - -// ========================================================== -// MISC FUNCTIONS -// ========================================================== - -// This could be used for entities as well. -stock ClientStopAllSlenderSounds(client, const String:profileName[], const String:sectionName[], iChannel) -{ - if (!client || !IsValidEntity(client)) return; - - if (!IsProfileValid(profileName)) return; - - decl String:buffer[PLATFORM_MAX_PATH]; - - KvRewind(g_hConfig); - if (KvJumpToKey(g_hConfig, profileName)) - { - decl String:s[32]; - - if (KvJumpToKey(g_hConfig, sectionName)) - { - for (new i2 = 1;; i2++) - { - IntToString(i2, s, sizeof(s)); - KvGetString(g_hConfig, s, buffer, sizeof(buffer)); - if (!buffer[0]) break; - - StopSound(client, iChannel, buffer); - } - } - } -} - -stock ClientUpdateListeningFlags(client, bool:bReset=false) -{ - if (!IsClientInGame(client)) return; - - for (new i = 1; i <= MaxClients; i++) - { - if (i == client || !IsClientInGame(i)) continue; - - if (bReset || IsRoundEnding() || GetConVarBool(g_cvAllChat)) - { - SetListenOverride(client, i, Listen_Default); - continue; - } - - new MuteMode:iMuteMode = g_iPlayerPreferences[client][PlayerPreference_MuteMode]; - - if (g_bPlayerEliminated[client]) - { - if (!g_bPlayerEliminated[i]) - { - if (iMuteMode == MuteMode_DontHearOtherTeam) - { - SetListenOverride(client, i, Listen_No); - } - else if (iMuteMode == MuteMode_DontHearOtherTeamIfNotProxy && !g_bPlayerProxy[client]) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - if (!g_bPlayerEliminated[i]) - {/* - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (DidClientEscape(i)) - { - if (!DidClientEscape(client)) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - if (!DidClientEscape(client)) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - } - else - {*/ - new bool:bCanHear = false; - if (GetConVarFloat(g_cvPlayerVoiceDistance) <= 0.0) bCanHear = true; - - if (!bCanHear) - { - decl Float:flMyPos[3], Float:flHisPos[3]; - GetClientEyePosition(client, flMyPos); - GetClientEyePosition(i, flHisPos); - - new Float:flDist = GetVectorDistance(flMyPos, flHisPos); - - if (GetConVarFloat(g_cvPlayerVoiceWallScale) > 0.0) - { - new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceRayDontHitCharacters); - new bool:bDidHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bDidHit) - { - flDist *= GetConVarFloat(g_cvPlayerVoiceWallScale); - } - } - - if (flDist <= GetConVarFloat(g_cvPlayerVoiceDistance)) - { - bCanHear = true; - } - } - - if (bCanHear) - { - if (IsClientInGhostMode(i) != IsClientInGhostMode(client) && - DidClientEscape(i) != DidClientEscape(client)) - { - bCanHear = false; - } - } - - if (bCanHear) - { - SetListenOverride(client, i, Listen_Default); - } - else - { - SetListenOverride(client, i, Listen_No); - } - //} - } - else - { - SetListenOverride(client, i, Listen_No); - } - } - } -} - -stock ClientShowMainMessage(client, const String:sMessage[], any:...) -{ - decl String:message[512]; - VFormat(message, sizeof(message), sMessage, 3); - - SetHudTextParams(-1.0, 0.4, - 5.0, - 255, - 255, - 255, - 200, - 2, - 1.0, - 0.07, - 2.0); - ShowSyncHudText(client, g_hHudSync, message); -} - -stock ClientResetSlenderStats(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSlenderStats(%d)", client); -#endif - - g_flPlayerStress[client] = 0.0; - g_flPlayerStressNextUpdateTime[client] = -1.0; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_bPlayerSeesSlender[client][i] = false; - g_flPlayerSeesSlenderLastTime[client][i] = -1.0; - g_flPlayerSightSoundNextTime[client][i] = -1.0; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSlenderStats(%d)", client); -#endif -} - -bool:ClientSetQueuePoints(client, iAmount) -{ - if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return false; - g_iPlayerQueuePoints[client] = iAmount; - ClientSaveCookies(client); - return true; -} - -ClientSaveCookies(client) -{ - if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return; - - // Save and reset our queue points. - decl String:s[64]; - Format(s, sizeof(s), "%d ; %d ; %d ; 0 ; %d", g_iPlayerQueuePoints[client], - g_iPlayerPreferences[client][PlayerPreference_ShowHints], - g_iPlayerPreferences[client][PlayerPreference_MuteMode], - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection]); - - SetClientCookie(client, g_hCookie, s); -} - -stock ClientViewPunch(client, const Float:angleOffset[3]) -{ - if (g_offsPlayerPunchAngleVel == -1) return; - - decl Float:flOffset[3]; - for (new i = 0; i < 3; i++) flOffset[i] = angleOffset[i]; - ScaleVector(flOffset, 20.0); - - /* - if (!IsFakeClient(client)) - { - // Latency compensation. - new Float:flLatency = GetClientLatency(client, NetFlow_Outgoing); - new Float:flLatencyCalcDiff = 60.0 * Pow(flLatency, 2.0); - - for (new i = 0; i < 3; i++) flOffset[i] += (flOffset[i] * flLatencyCalcDiff); - } - */ - - decl Float:flAngleVel[3]; - GetEntDataVector(client, g_offsPlayerPunchAngleVel, flAngleVel); - AddVectors(flAngleVel, flOffset, flOffset); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flOffset, true); -} - -public Action:Hook_ConstantGlowSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (EntRefToEntIndex(g_iPlayerConstantGlowEntity[i]) == ent) - { - iOwner = i; - break; - } - } - - if (iOwner != -1) - { - if (!IsPlayerAlive(iOwner) || g_bPlayerEliminated[iOwner]) return Plugin_Handled; - if (!IsPlayerAlive(other) || (!g_bPlayerProxy[other] && !IsClientInGhostMode(other))) return Plugin_Handled; - } - - return Plugin_Continue; -} - -stock ClientSetFOV(client, iFOV) -{ - SetEntData(client, g_offsPlayerFOV, iFOV); - SetEntData(client, g_offsPlayerDefaultFOV, iFOV); -} - -stock TF2_GetClassName(TFClassType:iClass, String:sBuffer[], sBufferLen) -{ - switch (iClass) - { - case TFClass_Scout: strcopy(sBuffer, sBufferLen, "scout"); - case TFClass_Sniper: strcopy(sBuffer, sBufferLen, "sniper"); - case TFClass_Soldier: strcopy(sBuffer, sBufferLen, "soldier"); - case TFClass_DemoMan: strcopy(sBuffer, sBufferLen, "demoman"); - case TFClass_Heavy: strcopy(sBuffer, sBufferLen, "heavyweapons"); - case TFClass_Medic: strcopy(sBuffer, sBufferLen, "medic"); - case TFClass_Pyro: strcopy(sBuffer, sBufferLen, "pyro"); - case TFClass_Spy: strcopy(sBuffer, sBufferLen, "spy"); - case TFClass_Engineer: strcopy(sBuffer, sBufferLen, "engineer"); - default: strcopy(sBuffer, sBufferLen, ""); - } -} - -#define EF_DIMLIGHT (1 << 2) - -stock ClientSDKFlashlightTurnOn(client) -{ - if (!IsValidClient(client)) return; - - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (iEffects & EF_DIMLIGHT) return; - - iEffects |= EF_DIMLIGHT; - - SetEntProp(client, Prop_Send, "m_fEffects", iEffects); -} - -stock ClientSDKFlashlightTurnOff(client) -{ - if (!IsValidClient(client)) return; - - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (!(iEffects & EF_DIMLIGHT)) return; - - iEffects &= ~EF_DIMLIGHT; - - SetEntProp(client, Prop_Send, "m_fEffects", iEffects); -} - -stock bool:IsPointVisibleToAPlayer(const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false) -{ - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (IsPointVisibleToPlayer(i, pos, bCheckFOV, bCheckBlink)) return true; - } - - return false; -} - -stock bool:IsPointVisibleToPlayer(client, const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client) || IsClientInGhostMode(client)) return false; - - if (bCheckEliminated && g_bPlayerEliminated[client]) return false; - - if (bCheckBlink && IsClientBlinking(client)) return false; - - decl Float:eyePos[3]; - GetClientEyePosition(client, eyePos); - - // Check fog, if we can. - if (g_offsPlayerFogCtrl != -1 && g_offsFogCtrlEnable != -1 && g_offsFogCtrlEnd != -1) - { - new iFogEntity = GetEntDataEnt2(client, g_offsPlayerFogCtrl); - if (IsValidEdict(iFogEntity)) - { - if (GetEntData(iFogEntity, g_offsFogCtrlEnable) && - GetVectorDistance(eyePos, pos) >= GetEntDataFloat(iFogEntity, g_offsFogCtrlEnd)) - { - return false; - } - } - } - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, pos, CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); - new bool:bHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bHit) return false; - - if (bCheckFOV) - { - decl Float:eyeAng[3], Float:reqVisibleAng[3]; - GetClientEyeAngles(client, eyeAng); - - new Float:flFOV = float(g_iPlayerDesiredFOV[client]); - SubtractVectors(pos, eyePos, reqVisibleAng); - GetVectorAngles(reqVisibleAng, reqVisibleAng); - - new Float:difference = FloatAbs(AngleDiff(eyeAng[0], reqVisibleAng[0])) + FloatAbs(AngleDiff(eyeAng[1], reqVisibleAng[1])); - if (difference > ((flFOV * 0.5) + 10.0)) return false; - } - - return true; -} - -public Action:Timer_ClientPostWeapons(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!IsPlayerAlive(client)) return; - - if (timer != g_hPlayerPostWeaponsTimer[client]) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) - { - DebugMessage("START Timer_ClientPostWeapons(%d)", client); - } - - new iOldWeaponItemIndexes[6] = { -1, ... }; - new iNewWeaponItemIndexes[6] = { -1, ... }; - - for (new i = 0; i <= 5; i++) - { - new iWeapon = GetPlayerWeaponSlot(client, i); - if (!IsValidEdict(iWeapon)) continue; - - iOldWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - } - -#endif - - new bool:bRemoveWeapons = true; - new bool:bRestrictWeapons = true; - - if (IsRoundEnding()) - { - if (!g_bPlayerEliminated[client]) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - } - - // pvp - if (IsClientInPvP(client)) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - - if (IsRoundInWarmup()) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - - if (IsClientInGhostMode(client)) - { - bRemoveWeapons = true; - } - - if (bRemoveWeapons) - { - if(!IsClientInGhostMode(client)) { - for (new i = 0; i <= 5; i++) - { - TF2_RemoveWeaponSlotAndWearables(client, i); - if (i == TFWeaponSlot_Melee) - { - // Give scout bat to every player to fix camera displacement. - new iNewWeapon = TF2Items_GiveNamedItem(client, g_hSDKWeaponBat); - EquipPlayerWeapon(client, iNewWeapon); - } - } - } - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_weapon_builder")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - - ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_wearable_demoshield")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - - ClientSwitchToWeaponSlot(client, TFWeaponSlot_Melee); - } - - if (bRestrictWeapons) - { - new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); - - if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) - { - new TFClassType:iPlayerClass = TF2_GetPlayerClass(client); - new Handle:hItem = INVALID_HANDLE; - - new iWeapon = INVALID_ENT_REFERENCE; - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - - if (IsValidEdict(iWeapon)) - { - if (IsWeaponRestricted(iPlayerClass, GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"))) - { - hItem = INVALID_HANDLE; - TF2_RemoveWeaponSlotAndWearables(client, iSlot); - - switch (iSlot) - { - case TFWeaponSlot_Primary: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponScattergun; - case TFClass_Sniper: hItem = g_hSDKWeaponSniperRifle; - case TFClass_Soldier: hItem = g_hSDKWeaponRocketLauncher; - case TFClass_DemoMan: hItem = g_hSDKWeaponGrenadeLauncher; - case TFClass_Heavy: hItem = g_hSDKWeaponMinigun; - case TFClass_Medic: hItem = g_hSDKWeaponSyringeGun; - case TFClass_Pyro: hItem = g_hSDKWeaponFlamethrower; - case TFClass_Spy: hItem = g_hSDKWeaponRevolver; - case TFClass_Engineer: hItem = g_hSDKWeaponShotgunPrimary; - } - } - case TFWeaponSlot_Secondary: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponPistolScout; - case TFClass_Sniper: hItem = g_hSDKWeaponSMG; - case TFClass_Soldier: hItem = g_hSDKWeaponShotgunSoldier; - case TFClass_DemoMan: hItem = g_hSDKWeaponStickyLauncher; - case TFClass_Heavy: hItem = g_hSDKWeaponShotgunHeavy; - case TFClass_Medic: hItem = g_hSDKWeaponMedigun; - case TFClass_Pyro: hItem = g_hSDKWeaponShotgunPyro; - case TFClass_Engineer: hItem = g_hSDKWeaponPistol; - } - } - case TFWeaponSlot_Melee: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponBat; - case TFClass_Sniper: hItem = g_hSDKWeaponKukri; - case TFClass_Soldier: hItem = g_hSDKWeaponShovel; - case TFClass_DemoMan: hItem = g_hSDKWeaponBottle; - case TFClass_Heavy: hItem = g_hSDKWeaponFists; - case TFClass_Medic: hItem = g_hSDKWeaponBonesaw; - case TFClass_Pyro: hItem = g_hSDKWeaponFireaxe; - case TFClass_Spy: hItem = g_hSDKWeaponKnife; - case TFClass_Engineer: hItem = g_hSDKWeaponWrench; - } - } - case 4: - { - switch (iPlayerClass) - { - case TFClass_Spy: hItem = g_hSDKWeaponInvis; - } - } - } - - if (hItem != INVALID_HANDLE) - { - new iNewWeapon = TF2Items_GiveNamedItem(client, hItem); - if (IsValidEntity(iNewWeapon)) - { - EquipPlayerWeapon(client, iNewWeapon); - } - } - } - } - } - } - - // Fixes the Pretty Boy's Pocket Pistol glitch. - new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, client); - if (iHealth > iMaxHealth) - { - SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); - SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); - } - } - - // Change stats on some weapons. - if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) - { - new iWeapon = INVALID_ENT_REFERENCE; - decl Handle:hWeapon; - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; - - new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - switch (iItemDef) - { - case 214: // Powerjack - { - TF2_RemoveWeaponSlot(client, iSlot); - - hWeapon = PrepareItemHandle("tf_weapon_fireaxe", 214, 0, 0, "180 ; 20.0 ; 206 ; 1.33"); - new iEnt = TF2Items_GiveNamedItem(client, hWeapon); - CloseHandle(hWeapon); - EquipPlayerWeapon(client, iEnt); - } - } - } - } - - // Remove all hats. - if (IsClientInGhostMode(client)) - { - new ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_wearable")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - } - -#if defined DEBUG - for (new i = 0; i <= 5; i++) - { - new iWeapon = GetPlayerWeaponSlot(client, i); - if (!IsValidEdict(iWeapon)) continue; - - iNewWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - } - - if (GetConVarInt(g_cvDebugDetail) > 0) - { - for (new i = 0; i <= 5; i++) - { - DebugMessage("-> slot %d: %d (old: %d)", i, iNewWeaponItemIndexes[i], iOldWeaponItemIndexes[i]); - } - - DebugMessage("END Timer_ClientPostWeapons(%d) -> remove = %d, restrict = %d", client, bRemoveWeapons, bRestrictWeapons); - } -#endif -} - -public Action:Timer_ApplyCustomModel(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - - if (g_bPlayerProxy[client] && iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - // Set custom model, if any. - decl String:sBuffer[PLATFORM_MAX_PATH]; - decl String:sSectionName[64]; - - decl String:sClassName[64]; - TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); - - Format(sSectionName, sizeof(sSectionName), "mod_proxy_%s", sClassName); - if ((GetRandomStringFromProfile(sProfile, sSectionName, sBuffer, sizeof(sBuffer)) && sBuffer[0]) || - (GetRandomStringFromProfile(sProfile, "mod_proxy_all", sBuffer, sizeof(sBuffer)) && sBuffer[0])) - { - SetVariantString(sBuffer); - AcceptEntityInput(client, "SetCustomModel"); - SetEntProp(client, Prop_Send, "m_bUseClassAnimations", true); - } - - if (IsPlayerAlive(client)) - { - // Play any sounds, if any. - if (GetRandomStringFromProfile(sProfile, "sound_proxy_spawn", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_spawn_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_spawn_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_spawn_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_spawn_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_spawn_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } -} - -bool:IsWeaponRestricted(TFClassType:iClass, iItemDef) -{ - if (g_hRestrictedWeaponsConfig == INVALID_HANDLE) return false; - - new bool:bReturn = false; - - decl String:sItemDef[32]; - IntToString(iItemDef, sItemDef, sizeof(sItemDef)); - - KvRewind(g_hRestrictedWeaponsConfig); - if (KvJumpToKey(g_hRestrictedWeaponsConfig, "all")) - { - bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef); - } - - new bool:bFoundSection = false; - KvRewind(g_hRestrictedWeaponsConfig); - - switch (iClass) - { - case TFClass_Scout: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "scout"); - case TFClass_Soldier: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "soldier"); - case TFClass_Sniper: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "sniper"); - case TFClass_DemoMan: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "demoman"); - case TFClass_Heavy: - { - bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavy"); - - if (!bFoundSection) - { - KvRewind(g_hRestrictedWeaponsConfig); - bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavyweapons"); - } - } - case TFClass_Medic: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "medic"); - case TFClass_Spy: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "spy"); - case TFClass_Pyro: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "pyro"); - case TFClass_Engineer: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "engineer"); - } - - if (bFoundSection) - { - bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef, bReturn); - } - - return bReturn; -} - -public Action:Timer_RespawnPlayer(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (IsPlayerAlive(client)) return; - - TF2_RespawnPlayer(client); +#if defined _sf2_client_included + #endinput +#endif +#define _sf2_client_included + +#define GHOST_MODEL "models/props_halloween/ghost_no_hat.mdl" +#define SF2_OVERLAY_DEFAULT "overlays/rytp_horror/grain" +#define SF2_OVERLAY_DEFAULT_NO_FILMGRAIN "overlays/rytp_horror/grain" // TODO: Update material? +#define SF2_OVERLAY_GHOST "overlays/rytp_horror/grain" + +#define SF2_FLASHLIGHT_WIDTH 512.0 // How wide the player's Flashlight should be in world units. +#define SF2_FLASHLIGHT_LENGTH 1024.0 // How far the player's Flashlight can reach in world units. +#define SF2_FLASHLIGHT_BRIGHTNESS 0 // Intensity of the players' Flashlight. +#define SF2_FLASHLIGHT_DRAIN_RATE 0.65 // How long (in seconds) each bar on the player's Flashlight meter lasts. +#define SF2_FLASHLIGHT_RECHARGE_RATE 0.68 // How long (in seconds) it takes each bar on the player's Flashlight meter to recharge. +#define SF2_FLASHLIGHT_FLICKERAT 0.25 // The percentage of the Flashlight battery where the Flashlight will start to blink. +#define SF2_FLASHLIGHT_ENABLEAT 0.3 // The percentage of the Flashlight battery where the Flashlight will be able to be used again (if the player shortens out the Flashlight from excessive use). +#define SF2_FLASHLIGHT_COOLDOWN 0.4 // How much time players have to wait before being able to switch their flashlight on again after turning it off. + +#define SF2_ULTRAVISION_WIDTH 800.0 +#define SF2_ULTRAVISION_LENGTH 800.0 +#define SF2_ULTRAVISION_BRIGHTNESS -4 // Intensity of Ultravision. +#define SF2_ULTRAVISION_CONE 180.0 + +#define SF2_PLAYER_BREATH_COOLDOWN_MIN 0.8 +#define SF2_PLAYER_BREATH_COOLDOWN_MAX 2.0 + +new String:g_strPlayerBreathSounds[][] = +{ + "rytp_horror/player_breath_1.wav" +}; + +static String:g_strPlayerLagCompensationWeapons[][] = +{ + "tf_weapon_sniperrifle", + "tf_weapon_sniperrifle_decap", + "tf_weapon_sniperrifle_classic" +}; + +// Deathcam data. +static g_iPlayerDeathCamBoss[MAXPLAYERS + 1] = { -1, ... }; +static bool:g_bPlayerDeathCam[MAXPLAYERS + 1] = { false, ... }; +static bool:g_bPlayerDeathCamShowOverlay[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerDeathCamEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerDeathCamEnt2[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Handle:g_hPlayerDeathCamTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Flashlight data. +static bool:g_bPlayerFlashlight[MAXPLAYERS + 1] = { false, ... }; +static bool:g_bPlayerFlashlightBroken[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerFlashlightEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerFlashlightEntAng[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Float:g_flPlayerFlashlightBatteryLife[MAXPLAYERS + 1] = { 1.0, ... }; +static Handle:g_hPlayerFlashlightBatteryTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerFlashlightNextInputTime[MAXPLAYERS + 1] = { -1.0, ... }; + +// Ultravision data. +static bool:g_bPlayerUltravision[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerUltravisionEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; + +// Sprint data. +static bool:g_bPlayerSprint[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerSprintPoints[MAXPLAYERS + 1] = { 100, ... }; +static Handle:g_hPlayerSprintTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Blink data. +static Handle:g_hPlayerBlinkTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static bool:g_bPlayerBlink[MAXPLAYERS + 1] = { false, ... }; +static Float:g_flPlayerBlinkMeter[MAXPLAYERS + 1] = { 0.0, ... }; +static g_iPlayerBlinkCount[MAXPLAYERS + 1] = { 0, ... }; + +// Breathing data. +static bool:g_bPlayerBreath[MAXPLAYERS + 1] = { false, ... }; +static Handle:g_hPlayerBreathTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Interactive glow data. +static g_iPlayerInteractiveGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerInteractiveGlowTargetEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; + +// Constant glow data. +static g_iPlayerConstantGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static bool:g_bPlayerConstantGlowEnabled[MAXPLAYERS + 1] = { false, ... }; + +// Jumpscare data. +static g_iPlayerJumpScareBoss[MAXPLAYERS + 1] = { -1, ... }; +static Float:g_flPlayerJumpScareLifeTime[MAXPLAYERS + 1] = { -1.0, ... }; + +static Float:g_flPlayerScareBoostEndTime[MAXPLAYERS + 1] = { -1.0, ... }; + +// Anti-camping data. +static g_iPlayerCampingStrikes[MAXPLAYERS + 1] = { 0, ... }; +static Handle:g_hPlayerCampingTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerCampingLastPosition[MAXPLAYERS + 1][3]; +static bool:g_bPlayerCampingFirstTime[MAXPLAYERS + 1] = { true, ... }; + + +// ========================================================== +// GENERAL CLIENT HOOK FUNCTIONS +// ========================================================== + +#define SF2_PLAYER_VIEWBOB_TIMER 10.0 +#define SF2_PLAYER_VIEWBOB_SCALE_X 0.05 +#define SF2_PLAYER_VIEWBOB_SCALE_Y 0.0 +#define SF2_PLAYER_VIEWBOB_SCALE_Z 0.0 + + +public MRESReturn:Hook_ClientWantsLagCompensationOnEntity(thisPointer, Handle:hReturn, Handle:hParams) +{ + if (!g_bEnabled || IsFakeClient(thisPointer)) return MRES_Ignored; + + DHookSetReturn(hReturn, true); + return MRES_Supercede; +} + +Float:ClientGetScareBoostEndTime(client) +{ + return g_flPlayerScareBoostEndTime[client]; +} + +ClientSetScareBoostEndTime(client, Float:time) +{ + g_flPlayerScareBoostEndTime[client] = time; +} + +public Hook_ClientPreThink(client) +{ + if (!g_bEnabled) return; + + ClientProcessViewAngles(client); + ClientProcessVisibility(client); + ClientProcessStaticShake(client); + ClientProcessFlashlightAngles(client); + ClientProcessInteractiveGlow(client); + + if (IsClientInGhostMode(client)) + { + SetEntPropFloat(client, Prop_Send, "m_flNextAttack", GetGameTime() + 2.0); + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 520.0); + } + else if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) + { + if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) + { + new iRoundState = _:GameRules_GetRoundState(); + + // No double jumping for players in play. + SetEntProp(client, Prop_Send, "m_iAirDash", 99999); + + if (!g_bPlayerProxy[client]) + { + if (iRoundState == 4) + { + new bool:bDanger = false; + + if (!bDanger) + { + decl iState; + decl iBossTarget; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetType(i) == SF2BossType_Chaser) + { + iBossTarget = EntRefToEntIndex(g_iSlenderTarget[i]); + iState = g_iSlenderState[i]; + + if ((iState == STATE_CHASE || iState == STATE_ATTACK || iState == STATE_STUN) && + ((iBossTarget && iBossTarget != INVALID_ENT_REFERENCE && (iBossTarget == client || ClientGetDistanceFromEntity(client, iBossTarget) < 512.0)) || NPCGetDistanceFromEntity(i, client) < 512.0 || PlayerCanSeeSlender(client, i, false))) + { + bDanger = true; + ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); + + // Induce client stress levels. + new Float:flUnComfortZoneDist = 512.0; + new Float:flStressScalar = (flUnComfortZoneDist / NPCGetDistanceFromEntity(i, client)); + ClientAddStress(client, 0.025 * flStressScalar); + + break; + } + } + } + } + + if (g_flPlayerStaticAmount[client] > 0.4) bDanger = true; + if (GetGameTime() < ClientGetScareBoostEndTime(client)) bDanger = true; + + if (!bDanger) + { + decl iState; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetType(i) == SF2BossType_Chaser) + { + if (iState == STATE_ALERT) + { + if (PlayerCanSeeSlender(client, i)) + { + bDanger = true; + ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); + } + } + } + } + } + + if (!bDanger) + { + new Float:flCurTime = GetGameTime(); + new Float:flScareSprintDuration = 3.0; + if (TF2_GetPlayerClass(client) == TFClass_DemoMan) flScareSprintDuration *= 1.667; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if ((flCurTime - g_flPlayerScareLastTime[client][i]) <= flScareSprintDuration) + { + bDanger = true; + break; + } + } + } + + new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); + new Float:flSprintSpeed = ClientGetDefaultSprintSpeed(client); + + // Check for weapon speed changes. + new iWeapon = INVALID_ENT_REFERENCE; + + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; + + new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + switch (iItemDef) + { + case 239: // Gloves of Running Urgently + { + if (GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon") == iWeapon) + { + flSprintSpeed += (flSprintSpeed * 0.1); + } + } + case 775: // Escape Plan + { + new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); + new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); + new Float:flPercentage = flHealth / flMaxHealth; + + if (flPercentage < 0.805 && flPercentage >= 0.605) flSprintSpeed += (flSprintSpeed * 0.05); + else if (flPercentage < 0.605 && flPercentage >= 0.405) flSprintSpeed += (flSprintSpeed * 0.1); + else if (flPercentage < 0.405 && flPercentage >= 0.205) flSprintSpeed += (flSprintSpeed * 0.15); + else if (flPercentage < 0.205) flSprintSpeed += (flSprintSpeed * 0.2); + } + } + } + + // Speed buff? + if (TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly)) + { + flWalkSpeed += (flWalkSpeed * 0.08); + flSprintSpeed += (flSprintSpeed * 0.08); + } + + if (bDanger) + { + flWalkSpeed *= 1.33; + flSprintSpeed *= 1.33; + + if (!g_bPlayerHints[client][PlayerHint_Sprint]) + { + ClientShowHint(client, PlayerHint_Sprint); + } + } + + new Float:flSprintSpeedSubtract = ((flSprintSpeed - flWalkSpeed) * 0.5); + flSprintSpeedSubtract -= flSprintSpeedSubtract * (g_iPlayerSprintPoints[client] != 0 ? (float(g_iPlayerSprintPoints[client]) / 100.0) : 0.0); + flSprintSpeed -= flSprintSpeedSubtract; + + if (IsClientSprinting(client)) + { + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flSprintSpeed); + } + else + { + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flWalkSpeed); + } + + if (ClientCanBreath(client) && !g_bPlayerBreath[client]) + { + ClientStartBreathing(client); + } + } + } + else + { + new TFClassType:iClass = TF2_GetPlayerClass(client); + new bool:bSpeedup = TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly); + + switch (iClass) + { + case TFClass_Scout: + { + if (iRoundState == 4) + { + if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 405.0); + else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); + } + } + case TFClass_Medic: + { + if (iRoundState == 4) + { + if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 385.0); + else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); + } + } + } + } + } + } + + // Calculate player stress levels. + if (GetGameTime() >= g_flPlayerStressNextUpdateTime[client]) + { + //new Float:flPagePercent = g_iPageMax != 0 ? float(g_iPageCount) / float(g_iPageMax) : 0.0; + //new Float:flPageCountPercent = g_iPageMax != 0? float(g_iPlayerPageCount[client]) / float(g_iPageMax) : 0.0; + + g_flPlayerStressNextUpdateTime[client] = GetGameTime() + 0.33; + ClientAddStress(client, -0.01); + +#if defined DEBUG + SendDebugMessageToPlayer(client, DEBUG_PLAYER_STRESS, 1, "g_flPlayerStress[%d]: %0.1f", client, g_flPlayerStress[client]); +#endif + } + + // Process screen shake, if enabled. + if (g_bPlayerShakeEnabled) + { + new bool:bDoShake = false; + + if (IsPlayerAlive(client)) + { + new iStaticMaster = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); + if (iStaticMaster != -1 && NPCGetFlags(iStaticMaster) & SFF_HASVIEWSHAKE) + { + bDoShake = true; + } + } + + if (bDoShake) + { + new Float:flPercent = g_flPlayerStaticAmount[client]; + + new Float:flAmplitudeMax = GetConVarFloat(g_cvPlayerShakeAmplitudeMax); + new Float:flAmplitude = flAmplitudeMax * flPercent; + + new Float:flFrequencyMax = GetConVarFloat(g_cvPlayerShakeFrequencyMax); + new Float:flFrequency = flFrequencyMax * flPercent; + + UTIL_ScreenShake(client, flAmplitude, 0.5, flFrequency); + } + } +} + +public Action:Hook_ClientSetTransmit(client, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (other != client) + { + if (IsClientInGhostMode(client) && !IsClientInGhostMode(other)) return Plugin_Handled; + + if (!IsRoundEnding()) + { + // SPECIAL ROUND: Singleplayer + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (!g_bPlayerEliminated[client] && !g_bPlayerEliminated[other] && !DidClientEscape(other)) return Plugin_Handled; + } + + // pvp + if (IsClientInPvP(client) && IsClientInPvP(other)) + { + if (TF2_IsPlayerInCondition(client, TFCond_Cloaked) && + !TF2_IsPlayerInCondition(client, TFCond_CloakFlicker) && + !TF2_IsPlayerInCondition(client, TFCond_Jarated) && + !TF2_IsPlayerInCondition(client, TFCond_Milked) && + !TF2_IsPlayerInCondition(client, TFCond_OnFire) && + (GetGameTime() > GetEntPropFloat(client, Prop_Send, "m_flInvisChangeCompleteTime"))) + { + return Plugin_Handled; + } + } + } + } + + return Plugin_Continue; +} + +public Action:TF2_CalcIsAttackCritical(client, weapon, String:sWeaponName[], &bool:result) +{ + if (!g_bEnabled) return Plugin_Continue; + + if ((IsRoundInWarmup() || IsClientInPvP(client)) && !IsRoundEnding()) + { + if (!GetConVarBool(g_cvPlayerFakeLagCompensation)) + { + new bool:bNeedsManualDamage = false; + + // Fake lag compensation isn't enabled; check to see if we need to deal damage manually. + for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) + { + if (StrEqual(sWeaponName, g_strPlayerLagCompensationWeapons[i], false)) + { + bNeedsManualDamage = true; + break; + } + } + + if (bNeedsManualDamage) + { + decl Float:flStartPos[3], Float:flEyeAng[3]; + GetClientEyePosition(client, flStartPos); + GetClientEyeAngles(client, flEyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flEyeAng, MASK_SHOT, RayType_Infinite, TraceRayDontHitEntity, client); + new iHitEntity = TR_GetEntityIndex(hTrace); + new iHitGroup = TR_GetHitGroup(hTrace); + CloseHandle(hTrace); + + if (IsValidClient(iHitEntity)) + { + if (GetClientTeam(iHitEntity) == GetClientTeam(client)) + { + if (IsRoundInWarmup() || IsClientInPvP(iHitEntity)) + { + new Float:flChargedDamage = GetEntPropFloat(weapon, Prop_Send, "m_flChargedDamage"); + if (flChargedDamage < 50.0) flChargedDamage = 50.0; + new iDamageType = DMG_BULLET; + + if (IsClientCritBoosted(client)) + { + result = true; + iDamageType |= DMG_ACID; + } + else if (iHitGroup == 1) + { + if (StrEqual(sWeaponName, "tf_weapon_sniperrifle_classic", false)) + { + if (flChargedDamage >= 150.0) + { + result = true; + iDamageType |= DMG_ACID; + } + } + else + { + if (TF2_IsPlayerInCondition(client, TFCond_Zoomed)) + { + result = true; + iDamageType |= DMG_ACID; + } + } + } + + SDKHooks_TakeDamage(iHitEntity, client, client, flChargedDamage, iDamageType); + return Plugin_Changed; + } + } + } + } + } + } + + return Plugin_Continue; +} + +public Action:Hook_ClientOnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsRoundInWarmup()) return Plugin_Continue; + + if (attacker != victim && IsValidClient(attacker)) + { + if (!IsRoundEnding()) + { + if (IsClientInPvP(victim) && IsClientInPvP(attacker)) + { + if (attacker == inflictor) + { + if (IsValidEdict(weapon)) + { + decl String:sWeaponClass[64]; + GetEdictClassname(weapon, sWeaponClass, sizeof(sWeaponClass)); + + // Backstab check! + if ((StrEqual(sWeaponClass, "tf_weapon_knife", false) || (TF2_GetPlayerClass(attacker) == TFClass_Spy && StrEqual(sWeaponClass, "saxxy", false))) && + (damagecustom != TF_CUSTOM_TAUNT_FENCING)) + { + decl Float:flMyPos[3], Float:flHisPos[3], Float:flMyDirection[3]; + GetClientAbsOrigin(victim, flMyPos); + GetClientAbsOrigin(attacker, flHisPos); + GetClientEyeAngles(victim, flMyDirection); + GetAngleVectors(flMyDirection, flMyDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flMyDirection, flMyDirection); + ScaleVector(flMyDirection, 32.0); + AddVectors(flMyDirection, flMyPos, flMyDirection); + + decl Float:p[3], Float:s[3]; + MakeVectorFromPoints(flMyPos, flHisPos, p); + MakeVectorFromPoints(flMyPos, flMyDirection, s); + if (GetVectorDotProduct(p, s) <= 0.0) + { + damage = float(GetEntProp(victim, Prop_Send, "m_iHealth")) * 2.0; + + new Handle:hCvar = FindConVar("tf_weapon_criticals"); + if (hCvar != INVALID_HANDLE && GetConVarBool(hCvar)) damagetype |= DMG_ACID; + return Plugin_Changed; + } + } + } + } + } + /* + else if (g_bPlayerProxy[victim] || g_bPlayerProxy[attacker]) + { + if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) + { + damage = 0.0; + return Plugin_Changed; + } + + if (g_bPlayerProxy[attacker]) + { + new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, victim); + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[attacker]); + if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) + { + if (damagecustom == TF_CUSTOM_TAUNT_GRAND_SLAM || + damagecustom == TF_CUSTOM_TAUNT_FENCING || + damagecustom == TF_CUSTOM_TAUNT_ARROW_STAB || + damagecustom == TF_CUSTOM_TAUNT_GRENADE || + damagecustom == TF_CUSTOM_TAUNT_BARBARIAN_SWING || + damagecustom == TF_CUSTOM_TAUNT_ENGINEER_ARM || + damagecustom == TF_CUSTOM_TAUNT_ARMAGEDDON) + { + if (damage >= float(iMaxHealth)) damage = float(iMaxHealth) * 0.5; + else damage = 0.0; + } + else if (damagecustom == TF_CUSTOM_BACKSTAB) // Modify backstab damage. + { + damage = float(iMaxHealth) * GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy_backstab", 0.25); + if (damagetype & DMG_ACID) damage /= 3.0; + } + + g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitenemy"); + if (g_iPlayerProxyControl[attacker] > 100) + { + g_iPlayerProxyControl[attacker] = 100; + } + + damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy", 1.0); + } + + return Plugin_Changed; + } + else if (g_bPlayerProxy[victim]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[victim]); + if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) + { + g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitbyenemy"); + if (g_iPlayerProxyControl[attacker] > 100) + { + g_iPlayerProxyControl[attacker] = 100; + } + + damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_self", 1.0); + } + + return Plugin_Changed; + } + } + */ + else + { + damage = 0.0; + return Plugin_Changed; + } + } + else + { + if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) + { + damage = 0.0; + return Plugin_Changed; + } + } + + if (IsClientInGhostMode(victim)) + { + damage = 0.0; + return Plugin_Changed; + } + } + + return Plugin_Continue; +} + +public Action:Hook_TEFireBullets(const String:te_name[], const Players[], numClients, Float:delay) +{ + if (!g_bEnabled) return Plugin_Continue; + + new client = TE_ReadNum("m_iPlayer") + 1; + if (IsValidClient(client)) + { + if (GetConVarBool(g_cvPlayerFakeLagCompensation)) + { + if ((IsRoundInWarmup() || IsClientInPvP(client))) + { + ClientEnableFakeLagCompensation(client); + } + } + } + + return Plugin_Continue; +} + +ClientResetStatic(client) +{ + g_iPlayerStaticMaster[client] = -1; + g_hPlayerStaticTimer[client] = INVALID_HANDLE; + g_flPlayerStaticIncreaseRate[client] = 0.0; + g_flPlayerStaticDecreaseRate[client] = 0.0; + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + g_flPlayerLastStaticTime[client] = 0.0; + g_flPlayerLastStaticVolume[client] = 0.0; + g_bPlayerInStaticShake[client] = false; + g_iPlayerStaticShakeMaster[client] = -1; + g_flPlayerStaticShakeMinVolume[client] = 0.0; + g_flPlayerStaticShakeMaxVolume[client] = 0.0; + g_flPlayerStaticAmount[client] = 0.0; + + if (IsClientInGame(client)) + { + if (g_strPlayerStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticSound[client]); + if (g_strPlayerLastStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + if (g_strPlayerStaticShakeSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); + strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), ""); + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); +} + +ClientResetHints(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetHints(%d)", client); +#endif + + for (new i = 0; i < PlayerHint_MaxNum; i++) + { + g_bPlayerHints[client][i] = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetHints(%d)", client); +#endif +} + +ClientShowHint(client, iHint) +{ + g_bPlayerHints[client][iHint] = true; + + switch (iHint) + { + case PlayerHint_Sprint: PrintHintText(client, "%T", "SF2 Hint Sprint", client); + case PlayerHint_Flashlight: PrintHintText(client, "%T", "SF2 Hint Flashlight", client); + case PlayerHint_Blink: PrintHintText(client, "%T", "SF2 Hint Blink", client); + case PlayerHint_MainMenu: PrintHintText(client, "%T", "SF2 Hint Main Menu", client); + } +} + +bool:DidClientEscape(client) +{ + return g_bPlayerEscaped[client]; +} + +ClientEscape(client) +{ + if (DidClientEscape(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("START ClientEscape(%d)", client); +#endif + + g_bPlayerEscaped[client] = true; + + ClientResetBreathing(client); + ClientResetSprint(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientDisableConstantGlow(client); + + // Speed recalculation. Props to the creators of FF2/VSH for this snippet. + TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); + + HandlePlayerHUD(client); + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + CPrintToChatAll("%t", "SF2 Player Escaped", sName); + + CheckRoundWinConditions(); + + Call_StartForward(fOnClientEscape); + Call_PushCell(client); + Call_Finish(); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("END ClientEscape(%d)", client); +#endif +} + +public Action:Timer_TeleportPlayerToEscapePoint(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!DidClientEscape(client)) return; + + if (IsPlayerAlive(client)) + { + TeleportClientToEscapePoint(client); + } +} + +stock Float:ClientGetDistanceFromEntity(client, entity) +{ + decl Float:flStartPos[3], Float:flEndPos[3]; + GetClientAbsOrigin(client, flStartPos); + GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", flEndPos); + return GetVectorDistance(flStartPos, flEndPos); +} + +ClientEnableFakeLagCompensation(client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || g_bPlayerLagCompensation[client]) return; + + // Can only enable lag compensation if we're in either of these two teams only. + new iMyTeam = GetClientTeam(client); + if (iMyTeam != _:TFTeam_Red && iMyTeam != _:TFTeam_Blue) return; + + // Can only enable lag compensation if there are other active teammates around. This is to prevent spontaneous round restarting. + new iCount; + for (new i = 1; i <= MaxClients; i++) + { + if (i == client) continue; + + if (IsValidClient(i) && IsPlayerAlive(i)) + { + new iTeam = GetClientTeam(i); + if ((iTeam == _:TFTeam_Red || iTeam == _:TFTeam_Blue) && iTeam == iMyTeam) + { + iCount++; + } + } + } + + if (!iCount) return; + + // Can only enable lag compensation only for specific weapons. + new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + if (!IsValidEdict(iActiveWeapon)) return; + + decl String:sClassName[64]; + GetEdictClassname(iActiveWeapon, sClassName, sizeof(sClassName)); + + new bool:bCompensate = false; + for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) + { + if (StrEqual(sClassName, g_strPlayerLagCompensationWeapons[i], false)) + { + bCompensate = true; + break; + } + } + + if (!bCompensate) return; + + g_bPlayerLagCompensation[client] = true; + g_iPlayerLagCompensationTeam[client] = iMyTeam; + SetEntProp(client, Prop_Send, "m_iTeamNum", 0); +} + +ClientDisableFakeLagCompensation(client) +{ + if (!g_bPlayerLagCompensation[client]) return; + + SetEntProp(client, Prop_Send, "m_iTeamNum", g_iPlayerLagCompensationTeam[client]); + g_bPlayerLagCompensation[client] = false; + g_iPlayerLagCompensationTeam[client] = -1; +} + +// ========================================================== +// FLASHLIGHT / ULTRAVISION FUNCTIONS +// ========================================================== + +bool:IsClientUsingFlashlight(client) +{ + return g_bPlayerFlashlight[client]; +} + +Float:ClientGetFlashlightBatteryLife(client) +{ + return g_flPlayerFlashlightBatteryLife[client]; +} + +ClientSetFlashlightBatteryLife(client, Float:flPercent) +{ + g_flPlayerFlashlightBatteryLife[client] = flPercent; +} + +/** + * Called in Hook_ClientPreThink, this makes sure the flashlight is oriented correctly on the player. + */ +static ClientProcessFlashlightAngles(client) +{ + if (!IsClientInGame(client)) return; + + if (IsPlayerAlive(client)) + { + decl fl, Float:eyeAng[3], Float:ang2[3]; + + if (IsClientUsingFlashlight(client)) + { + fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + TeleportEntity(fl, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }, NULL_VECTOR); + } + + fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + GetClientEyeAngles(client, eyeAng); + GetClientAbsAngles(client, ang2); + SubtractVectors(eyeAng, ang2, eyeAng); + TeleportEntity(fl, NULL_VECTOR, eyeAng, NULL_VECTOR); + } + } + } +} + +/** + * Handles whether or not the player's flashlight should be "flickering", a sign of a dying flashlight battery. + */ +static ClientHandleFlashlightFlickerState(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) + { + new bool:bFlicker = bool:(ClientGetFlashlightBatteryLife(client) <= SF2_FLASHLIGHT_FLICKERAT); + + new fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + if (bFlicker) + { + SetEntProp(fl, Prop_Data, "m_LightStyle", 10); + } + else + { + SetEntProp(fl, Prop_Data, "m_LightStyle", 0); + } + } + + fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + if (bFlicker) + { + SetEntityRenderFx(fl, RenderFx:13); + } + else + { + SetEntityRenderFx(fl, RenderFx:0); + } + } + } +} + +bool:IsClientFlashlightBroken(client) +{ + return g_bPlayerFlashlightBroken[client]; +} + +Float:ClientGetFlashlightNextInputTime(client) +{ + return g_flPlayerFlashlightNextInputTime[client]; +} + +/** + * Breaks the player's flashlight. Nothing else. + */ +ClientBreakFlashlight(client) +{ + if (IsClientFlashlightBroken(client)) return; + + g_bPlayerFlashlightBroken[client] = true; + + ClientSetFlashlightBatteryLife(client, 0.0); + ClientTurnOffFlashlight(client); + + ClientAddStress(client, 0.2); + + EmitSoundToAll(FLASHLIGHT_BREAKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + + Call_StartForward(fOnClientBreakFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Resets everything of the player's flashlight. + */ +ClientResetFlashlight(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetFlashlight(%d)", client); +#endif + + ClientTurnOffFlashlight(client); + ClientSetFlashlightBatteryLife(client, 1.0); + g_bPlayerFlashlightBroken[client] = false; + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + g_flPlayerFlashlightNextInputTime[client] = -1.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetFlashlight(%d)", client); +#endif +} + +public Action:Hook_FlashlightSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEnt[other]) != ent) return Plugin_Handled; + + // We've already checked for flashlight ownership in the last statement. So we can do just this. + if (g_iPlayerPreferences[other][PlayerPreference_ProjectedFlashlight]) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Hook_Flashlight2SetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[other]) == ent) return Plugin_Handled; + return Plugin_Continue; +} + +public Hook_FlashlightEndSpawnPost(ent) +{ + if (!g_bEnabled) return; + + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightEndSetTransmit); + SDKUnhook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); +} + +public Action:Hook_FlashlightBeamSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + new iSpotlight = -1; + while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) + { + if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) + { + iOwner = iSpotlight; + break; + } + } + + if (iOwner == -1) return Plugin_Continue; + + new iClient = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) + { + iClient = i; + break; + } + } + + if (iClient == -1) return Plugin_Continue; + + if (iClient == other) + { + if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) + { + return Plugin_Handled; + } + } + else + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_FlashlightEndSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + new iSpotlight = -1; + while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) + { + if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) + { + iOwner = iSpotlight; + break; + } + } + + if (iOwner == -1) return Plugin_Continue; + + new iClient = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) + { + iClient = i; + break; + } + } + + if (iClient == -1) return Plugin_Continue; + + if (iClient == other) + { + if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) + { + return Plugin_Handled; + } + } + else + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Timer_DrainFlashlight(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; + + new iOverride = GetConVarInt(g_cvPlayerInfiniteFlashlightOverride); + if ((!g_bRoundInfiniteFlashlight && iOverride != 1) || iOverride == 0) + { + ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) - 0.01); + } + + if (ClientGetFlashlightBatteryLife(client) <= 0.0) + { + // Break the player's flashlight, but also start recharging. + ClientBreakFlashlight(client); + ClientStartRechargingFlashlightBattery(client); + ClientActivateUltravision(client); + return Plugin_Stop; + } + else + { + ClientHandleFlashlightFlickerState(client); + } + + return Plugin_Continue; +} + +public Action:Timer_RechargeFlashlight(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; + + ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) + 0.01); + + if (IsClientFlashlightBroken(client) && ClientGetFlashlightBatteryLife(client) >= SF2_FLASHLIGHT_ENABLEAT) + { + // Repair the flashlight. + g_bPlayerFlashlightBroken[client] = false; + } + + if (ClientGetFlashlightBatteryLife(client) >= 1.0) + { + // I am fully charged! + ClientSetFlashlightBatteryLife(client, 1.0); + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + + return Plugin_Stop; + } + + return Plugin_Continue; +} + +/** + * Turns on the player's flashlight. Nothing else. + */ +ClientTurnOnFlashlight(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) return; + + g_bPlayerFlashlight[client] = true; + + decl Float:flEyePos[3]; + GetClientEyePosition(client, flEyePos); + + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + // If the player is using the projected flashlight, just set effect flags. + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (!(iEffects & (1 << 2))) + { + SetEntProp(client, Prop_Send, "m_fEffects", iEffects | (1 << 2)); + } + } + else + { + // Spawn the light which only the user will see. + new ent = CreateEntityByName("light_dynamic"); + if (ent != -1) + { + TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); + DispatchKeyValue(ent, "targetname", "WUBADUBDUBMOTHERBUCKERS"); + DispatchKeyValue(ent, "rendercolor", "255 255 255"); + SetVariantFloat(SF2_FLASHLIGHT_WIDTH); + AcceptEntityInput(ent, "spotlight_radius"); + SetVariantFloat(SF2_FLASHLIGHT_LENGTH); + AcceptEntityInput(ent, "distance"); + SetVariantInt(SF2_FLASHLIGHT_BRIGHTNESS); + AcceptEntityInput(ent, "brightness"); + + // Convert WU to inches. + new Float:cone = 55.0; + cone *= 0.75; + + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_inner_cone"); + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_cone"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "TurnOn"); + + g_iPlayerFlashlightEnt[client] = EntIndexToEntRef(ent); + + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightSetTransmit); + } + } + + // Spawn the light that only everyone else will see. + new ent = CreateEntityByName("point_spotlight"); + if (ent != -1) + { + TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); + + decl String:sBuffer[256]; + FloatToString(SF2_FLASHLIGHT_LENGTH, sBuffer, sizeof(sBuffer)); + DispatchKeyValue(ent, "spotlightlength", sBuffer); + FloatToString(SF2_FLASHLIGHT_WIDTH, sBuffer, sizeof(sBuffer)); + DispatchKeyValue(ent, "spotlightwidth", sBuffer); + DispatchKeyValue(ent, "rendercolor", "255 255 255"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "LightOn"); + + g_iPlayerFlashlightEntAng[client] = EntIndexToEntRef(ent); + } + + Call_StartForward(fOnClientActivateFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Turns off the player's flashlight. Nothing else. + */ +ClientTurnOffFlashlight(client) +{ + if (!IsClientUsingFlashlight(client)) return; + + g_bPlayerFlashlight[client] = false; + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + + // Remove user-only light. + new ent = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "TurnOff"); + AcceptEntityInput(ent, "Kill"); + } + + // Remove everyone-else-only light. + ent = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "LightOff"); + CreateTimer(0.1, Timer_KillEntity, g_iPlayerFlashlightEntAng[client], TIMER_FLAG_NO_MAPCHANGE); + } + + g_iPlayerFlashlightEnt[client] = INVALID_ENT_REFERENCE; + g_iPlayerFlashlightEntAng[client] = INVALID_ENT_REFERENCE; + + if (IsClientInGame(client)) + { + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (iEffects & (1 << 2)) + { + SetEntProp(client, Prop_Send, "m_fEffects", iEffects &= ~(1 << 2)); + } + } + } + + Call_StartForward(fOnClientDeactivateFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +ClientStartRechargingFlashlightBattery(client) +{ + g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(SF2_FLASHLIGHT_RECHARGE_RATE, Timer_RechargeFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStartDrainingFlashlightBattery(client) +{ + new Float:flDrainRate = SF2_FLASHLIGHT_DRAIN_RATE; + if (TF2_GetPlayerClass(client) == TFClass_Engineer) + { + // Engineers have a 33% longer battery life, basically. + // TODO: Make this value customizable via cvar. + flDrainRate *= 1.33; + } + + g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(flDrainRate, Timer_DrainFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +ClientHandleFlashlight(client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) + { + ClientTurnOffFlashlight(client); + ClientStartRechargingFlashlightBattery(client); + ClientActivateUltravision(client); + + g_flPlayerFlashlightNextInputTime[client] = GetGameTime() + SF2_FLASHLIGHT_COOLDOWN; + + EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + } + else + { + // Only players in the "game" can use the flashlight. + if (!g_bPlayerEliminated[client]) + { + new bool:bCanUseFlashlight = true; + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_LIGHTSOUT) + { + // Unequip the flashlight please. + bCanUseFlashlight = false; + } + + if (!IsClientFlashlightBroken(client) && bCanUseFlashlight) + { + ClientTurnOnFlashlight(client); + ClientStartDrainingFlashlightBattery(client); + ClientDeactivateUltravision(client); + + g_flPlayerFlashlightNextInputTime[client] = GetGameTime(); + + EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + } + else + { + EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); + } + } + } +} + +bool:IsClientUsingUltravision(client) +{ + return g_bPlayerUltravision[client]; +} + +ClientActivateUltravision(client) +{ + if (!IsClientInGame(client) || IsClientUsingUltravision(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientActivateUltravision(%d)", client); +#endif + + g_bPlayerUltravision[client] = true; + + new ent = CreateEntityByName("light_dynamic"); + if (ent != -1) + { + decl Float:flEyePos[3]; + GetClientEyePosition(client, flEyePos); + + TeleportEntity(ent, flEyePos, Float:{ 90.0, 0.0, 0.0 }, NULL_VECTOR); + DispatchKeyValue(ent, "rendercolor", "0 200 255"); + + new Float:flRadius = 0.0; + if (g_bPlayerEliminated[client]) + { + flRadius = GetConVarFloat(g_cvUltravisionRadiusBlue); + } + else + { + flRadius = GetConVarFloat(g_cvUltravisionRadiusRed); + } + + SetVariantFloat(flRadius); + AcceptEntityInput(ent, "spotlight_radius"); + SetVariantFloat(flRadius); + AcceptEntityInput(ent, "distance"); + + SetVariantInt(-15); // Start dark, then fade in via the Timer_UltravisionFadeInEffect timer func. + AcceptEntityInput(ent, "brightness"); + + // Convert WU to inches. + new Float:cone = SF2_ULTRAVISION_CONE; + cone *= 0.75; + + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_inner_cone"); + SetVariantInt(0); + AcceptEntityInput(ent, "_cone"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "TurnOn"); + SetEntityRenderFx(ent, RENDERFX_SOLID_SLOW); + SetEntityRenderColor(ent, 100, 200, 255, 255); + + g_iPlayerUltravisionEnt[client] = EntIndexToEntRef(ent); + + SDKHook(ent, SDKHook_SetTransmit, Hook_UltravisionSetTransmit); + + // Fade in effect. + CreateTimer(0.0, Timer_UltravisionFadeInEffect, g_iPlayerUltravisionEnt[client], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientActivateUltravision(%d)", client); +#endif +} + +public Action:Timer_UltravisionFadeInEffect(Handle:timer, any:entref) +{ + new ent = EntRefToEntIndex(entref); + if (!ent || ent == INVALID_ENT_REFERENCE) return Plugin_Stop; + + new iBrightness = GetEntProp(ent, Prop_Send, "m_Exponent"); + if (iBrightness >= GetConVarInt(g_cvUltravisionBrightness)) return Plugin_Stop; + + iBrightness++; + SetVariantInt(iBrightness); + AcceptEntityInput(ent, "brightness"); + + return Plugin_Continue; +} + +ClientDeactivateUltravision(client) +{ + if (!IsClientUsingUltravision(client)) return; + + g_bPlayerUltravision[client] = false; + + new ent = EntRefToEntIndex(g_iPlayerUltravisionEnt[client]); + if (ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "TurnOff"); + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerUltravisionEnt[client] = INVALID_ENT_REFERENCE; +} + +public Action:Hook_UltravisionSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!GetConVarBool(g_cvUltravisionEnabled) || EntRefToEntIndex(g_iPlayerUltravisionEnt[other]) != ent || !IsPlayerAlive(other)) return Plugin_Handled; + return Plugin_Continue; +} + +static Float:ClientGetDefaultWalkSpeed(client) +{ + new Float:flReturn = 190.0; + new Float:flReturn2 = flReturn; + new Action:iAction = Plugin_Continue; + new TFClassType:iClass = TF2_GetPlayerClass(client); + + switch (iClass) + { + case TFClass_Scout: flReturn = 190.0; + case TFClass_Sniper: flReturn = 190.0; + case TFClass_Soldier: flReturn = 190.0; + case TFClass_DemoMan: flReturn = 190.0; + case TFClass_Heavy: flReturn = 190.0; + case TFClass_Medic: flReturn = 190.0; + case TFClass_Pyro: flReturn = 190.0; + case TFClass_Spy: flReturn = 190.0; + case TFClass_Engineer: flReturn = 190.0; + } + + // Call our forward. + Call_StartForward(fOnClientGetDefaultWalkSpeed); + Call_PushCell(client); + Call_PushCellRef(flReturn2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) flReturn = flReturn2; + + return flReturn; +} + +static Float:ClientGetDefaultSprintSpeed(client) +{ + new Float:flReturn = 300.0; + new Float:flReturn2 = flReturn; + new Action:iAction = Plugin_Continue; + new TFClassType:iClass = TF2_GetPlayerClass(client); + + switch (iClass) + { + case TFClass_Scout: flReturn = 300.0; + case TFClass_Sniper: flReturn = 300.0; + case TFClass_Soldier: flReturn = 275.0; + case TFClass_DemoMan: flReturn = 285.0; + case TFClass_Heavy: flReturn = 270.0; + case TFClass_Medic: flReturn = 300.0; + case TFClass_Pyro: flReturn = 300.0; + case TFClass_Spy: flReturn = 300.0; + case TFClass_Engineer: flReturn = 300.0; + } + + // Call our forward. + Call_StartForward(fOnClientGetDefaultSprintSpeed); + Call_PushCell(client); + Call_PushCellRef(flReturn2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) flReturn = flReturn2; + + return flReturn; +} + +// Static shaking should only affect the x, y portion of the player's view, not roll. +// This is purely for cosmetic effect. + +ClientProcessStaticShake(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + new bool:bOldStaticShake = g_bPlayerInStaticShake[client]; + new iOldStaticShakeMaster = NPCGetFromUniqueID(g_iPlayerStaticShakeMaster[client]); + new iNewStaticShakeMaster = -1; + new Float:flNewStaticShakeMasterAnger = -1.0; + + new Float:flOldPunchAng[3], Float:flOldPunchAngVel[3]; + GetEntDataVector(client, g_offsPlayerPunchAngle, flOldPunchAng); + GetEntDataVector(client, g_offsPlayerPunchAngleVel, flOldPunchAngVel); + + new Float:flNewPunchAng[3], Float:flNewPunchAngVel[3]; + + for (new i = 0; i < 3; i++) + { + flNewPunchAng[i] = flOldPunchAng[i]; + flNewPunchAngVel[i] = flOldPunchAngVel[i]; + } + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (g_iPlayerStaticMode[client][i] != Static_Increase) continue; + if (!(NPCGetFlags(i) & SFF_HASSTATICSHAKE)) continue; + + if (NPCGetAnger(i) > flNewStaticShakeMasterAnger) + { + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + if (iMaster == -1) iMaster = i; + + iNewStaticShakeMaster = iMaster; + flNewStaticShakeMasterAnger = NPCGetAnger(iMaster); + } + } + + if (iNewStaticShakeMaster != -1) + { + g_iPlayerStaticShakeMaster[client] = NPCGetUniqueID(iNewStaticShakeMaster); + + if (iNewStaticShakeMaster != iOldStaticShakeMaster) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iNewStaticShakeMaster, sProfile, sizeof(sProfile)); + + if (g_strPlayerStaticShakeSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + g_flPlayerStaticShakeMinVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_min", 0.0); + g_flPlayerStaticShakeMaxVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_max", 1.0); + + decl String:sStaticSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_shake_local", sStaticSound, sizeof(sStaticSound)); + if (sStaticSound[0]) + { + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), sStaticSound); + } + else + { + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); + } + } + } + + if (g_bPlayerInStaticShake[client]) + { + if (g_flPlayerStaticAmount[client] <= 0.0) + { + g_bPlayerInStaticShake[client] = false; + } + } + else + { + if (iNewStaticShakeMaster != -1) + { + g_bPlayerInStaticShake[client] = true; + } + } + + if (g_bPlayerInStaticShake[client] && !bOldStaticShake) + { + for (new i = 0; i < 2; i++) + { + flNewPunchAng[i] = 0.0; + flNewPunchAngVel[i] = 0.0; + } + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } + else if (!g_bPlayerInStaticShake[client] && bOldStaticShake) + { + for (new i = 0; i < 2; i++) + { + flNewPunchAng[i] = 0.0; + flNewPunchAngVel[i] = 0.0; + } + + g_iPlayerStaticShakeMaster[client] = -1; + + if (g_strPlayerStaticShakeSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); + + g_flPlayerStaticShakeMinVolume[client] = 0.0; + g_flPlayerStaticShakeMaxVolume[client] = 0.0; + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } + + if (g_bPlayerInStaticShake[client]) + { + if (g_strPlayerStaticShakeSound[client][0]) + { + new Float:flVolume = g_flPlayerStaticAmount[client]; + if (GetRandomFloat(0.0, 1.0) <= 0.35) + { + flVolume = 0.0; + } + else + { + if (flVolume < g_flPlayerStaticShakeMinVolume[client]) + { + flVolume = g_flPlayerStaticShakeMinVolume[client]; + } + + if (flVolume > g_flPlayerStaticShakeMaxVolume[client]) + { + flVolume = g_flPlayerStaticShakeMaxVolume[client]; + } + } + + EmitSoundToClient(client, g_strPlayerStaticShakeSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL | SND_STOP, flVolume); + } + + // Spazz our view all over the place. + for (new i = 0; i < 2; i++) flNewPunchAng[i] = AngleNormalize(GetRandomFloat(0.0, 360.0)); + NormalizeVector(flNewPunchAng, flNewPunchAng); + + new Float:flAngVelocityScalar = 5.0 * g_flPlayerStaticAmount[client]; + if (flAngVelocityScalar < 1.0) flAngVelocityScalar = 1.0; + ScaleVector(flNewPunchAng, flAngVelocityScalar); + + for (new i = 0; i < 2; i++) flNewPunchAngVel[i] = 0.0; + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } +} + +ClientProcessVisibility(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + new String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + new bool:bWasSeeingSlender[MAX_BOSSES]; + new iOldStaticMode[MAX_BOSSES]; + + decl Float:flSlenderPos[3]; + decl Float:flSlenderEyePos[3]; + decl Float:flSlenderOBBCenterPos[3]; + + decl Float:flMyPos[3]; + GetClientAbsOrigin(client, flMyPos); + + for (new i = 0; i < MAX_BOSSES; i++) + { + bWasSeeingSlender[i] = g_bPlayerSeesSlender[client][i]; + iOldStaticMode[i] = g_iPlayerStaticMode[client][i]; + g_bPlayerSeesSlender[client][i] = false; + g_iPlayerStaticMode[client][i] = Static_None; + + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + new iBoss = NPCGetEntIndex(i); + + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + SlenderGetAbsOrigin(i, flSlenderPos); + NPCGetEyePosition(i, flSlenderEyePos); + + decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; + GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flSlenderMins); + GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flSlenderMaxs); + + for (new i2 = 0; i2 < 3; i2++) flSlenderOBBCenterPos[i2] = flSlenderPos[i2] + ((flSlenderMins[i2] + flSlenderMaxs[i2]) / 2.0); + } + + if (IsClientInGhostMode(client)) + { + } + else if (!IsClientInDeathCam(client)) + { + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + + if (!IsPointVisibleToPlayer(client, flSlenderEyePos, true, SlenderUsesBlink(i))) + { + g_bPlayerSeesSlender[client][i] = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, true, SlenderUsesBlink(i)); + } + else + { + g_bPlayerSeesSlender[client][i] = true; + } + + if ((GetGameTime() - g_flPlayerSeesSlenderLastTime[client][i]) > GetProfileFloat(sProfile, "static_on_look_gracetime", 1.0) || + (iOldStaticMode[i] == Static_Increase && g_flPlayerStaticAmount[client] > 0.1)) + { + if ((NPCGetFlags(i) & SFF_STATICONLOOK) && + g_bPlayerSeesSlender[client][i]) + { + if (iCopyMaster != -1) + { + g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; + } + else + { + g_iPlayerStaticMode[client][i] = Static_Increase; + } + } + else if ((NPCGetFlags(i) & SFF_STATICONRADIUS) && + GetVectorDistance(flMyPos, flSlenderPos) <= g_flSlenderStaticRadius[i]) + { + new bool:bNoObstacles = IsPointVisibleToPlayer(client, flSlenderEyePos, false, false); + if (!bNoObstacles) bNoObstacles = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, false); + + if (bNoObstacles) + { + if (iCopyMaster != -1) + { + g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; + } + else + { + g_iPlayerStaticMode[client][i] = Static_Increase; + } + } + } + } + + // Process death cam sequence conditions + if (SlenderKillsOnNear(i)) + { + if (g_flPlayerStaticAmount[client] >= 1.0 || + GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetInstantKillRadius(i)) + { + new bool:bKillPlayer = true; + if (g_flPlayerStaticAmount[client] < 1.0) + { + bKillPlayer = IsPointVisibleToPlayer(client, flSlenderEyePos, false, SlenderUsesBlink(i)); + } + + if (!bKillPlayer) bKillPlayer = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, SlenderUsesBlink(i)); + + if (bKillPlayer) + { + g_flSlenderLastKill[i] = GetGameTime(); + + if (g_flPlayerStaticAmount[client] >= 1.0) + { + ClientStartDeathCam(client, NPCGetFromUniqueID(g_iPlayerStaticMaster[client]), flSlenderPos); + } + else + { + ClientStartDeathCam(client, i, flSlenderPos); + } + } + } + } + } + } + + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + if (iMaster == -1) iMaster = i; + + // Boss visiblity. + if (g_bPlayerSeesSlender[client][i] && !bWasSeeingSlender[i]) + { + g_flPlayerSeesSlenderLastTime[client][iMaster] = GetGameTime(); + + if (GetGameTime() >= g_flPlayerScareNextTime[client][iMaster]) + { + if (GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetScareRadius(i)) + { + ClientPerformScare(client, iMaster); + + if (NPCHasAttribute(iMaster, "ignite player on scare")) + { + new Float:flValue = NPCGetAttributeValue(iMaster, "ignite player on scare"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + } + else + { + g_flPlayerScareNextTime[client][iMaster] = GetGameTime() + GetProfileFloat(sProfile, "scare_cooldown"); + } + } + + if (NPCGetType(i) == SF2BossType_Static) + { + if (NPCGetFlags(i) & SFF_FAKE) + { + SlenderMarkAsFake(i); + return; + } + } + + Call_StartForward(fOnClientLooksAtBoss); + Call_PushCell(client); + Call_PushCell(i); + Call_Finish(); + } + else if (!g_bPlayerSeesSlender[client][i] && bWasSeeingSlender[i]) + { + g_flPlayerScareLastTime[client][iMaster] = GetGameTime(); + + Call_StartForward(fOnClientLooksAwayFromBoss); + Call_PushCell(client); + Call_PushCell(i); + Call_Finish(); + } + + if (g_bPlayerSeesSlender[client][i]) + { + if (GetGameTime() >= g_flPlayerSightSoundNextTime[client][iMaster]) + { + ClientPerformSightSound(client, i); + } + } + + if (g_iPlayerStaticMode[client][i] == Static_Increase && + iOldStaticMode[i] != Static_Increase) + { + if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) + { + decl String:sLoopSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); + + if (sLoopSound[0]) + { + EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, GetProfileNum(sProfile, "sound_static_loop_local_level", SNDLEVEL_NORMAL), SND_CHANGEVOL, 1.0); + ClientAddStress(client, 0.03); + } + else + { + LogError("Warning! Boss %s supports static loop local sounds, but was given a blank sound path!", sProfile); + } + } + } + else if (g_iPlayerStaticMode[client][i] != Static_Increase && + iOldStaticMode[i] == Static_Increase) + { + if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) + { + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + decl String:sLoopSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); + + if (sLoopSound[0]) + { + EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, _, SND_CHANGEVOL | SND_STOP, 0.0); + } + } + } + } + } + + // Initialize static timers. + new iBossLastStatic = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); + new iBossNewStatic = -1; + if (iBossLastStatic != -1 && g_iPlayerStaticMode[client][iBossLastStatic] == Static_Increase) + { + iBossNewStatic = iBossLastStatic; + } + + for (new i = 0; i < MAX_BOSSES; i++) + { + new iStaticMode = g_iPlayerStaticMode[client][i]; + + // Determine new static rates. + if (iStaticMode != Static_Increase) continue; + + if (iBossLastStatic == -1 || + g_iPlayerStaticMode[client][iBossLastStatic] != Static_Increase || + NPCGetAnger(i) > NPCGetAnger(iBossLastStatic)) + { + iBossNewStatic = i; + } + } + + if (iBossNewStatic != -1) + { + new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossNewStatic]); + if (iCopyMaster != -1) + { + iBossNewStatic = iCopyMaster; + g_iPlayerStaticMaster[client] = NPCGetUniqueID(iCopyMaster); + } + else + { + g_iPlayerStaticMaster[client] = NPCGetUniqueID(iBossNewStatic); + } + } + else + { + g_iPlayerStaticMaster[client] = -1; + } + + if (iBossNewStatic != iBossLastStatic) + { + if (!StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) + { + // Stop last-last static sound entirely. + if (g_strPlayerLastStaticSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + } + } + + // Move everything down towards the last arrays. + if (g_strPlayerStaticSound[client][0]) + { + strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), g_strPlayerStaticSound[client]); + } + + if (iBossNewStatic == -1) + { + // No one is the static master. + g_hPlayerStaticTimer[client] = CreateTimer(g_flPlayerStaticDecreaseRate[client], + Timer_ClientDecreaseStatic, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerStaticTimer[client], true); + } + else + { + NPCGetProfile(iBossNewStatic, sProfile, sizeof(sProfile)); + + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); + + new String:sStaticSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static", sStaticSound, sizeof(sStaticSound), 1); + + if (sStaticSound[0]) + { + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), sStaticSound); + } + + // Cross-fade out the static sounds. + g_flPlayerLastStaticVolume[client] = g_flPlayerStaticAmount[client]; + g_flPlayerLastStaticTime[client] = GetGameTime(); + + g_hPlayerLastStaticTimer[client] = CreateTimer(0.0, + Timer_ClientFadeOutLastStaticSound, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerLastStaticTimer[client], true); + + // Start up our own static timer. + new Float:flStaticIncreaseRate = GetProfileFloat(sProfile, "static_rate") / g_flRoundDifficultyModifier; + new Float:flStaticDecreaseRate = GetProfileFloat(sProfile, "static_rate_decay"); + + g_flPlayerStaticIncreaseRate[client] = flStaticIncreaseRate; + g_flPlayerStaticDecreaseRate[client] = flStaticDecreaseRate; + + g_hPlayerStaticTimer[client] = CreateTimer(flStaticIncreaseRate, + Timer_ClientIncreaseStatic, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerStaticTimer[client], true); + } + } +} + +ClientProcessViewAngles(client) +{ + if ((!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) && + !DidClientEscape(client)) + { + // Process view bobbing, if enabled. + // This code is based on the code in this page: https://developer.valvesoftware.com/wiki/Camera_Bob + // Many thanks to whomever created it in the first place. + + if (IsPlayerAlive(client)) + { + if (g_bPlayerViewbobEnabled) + { + new Float:flPunchVel[3]; + + if (!g_bPlayerViewbobSprintEnabled || !IsClientReallySprinting(client)) + { + if (GetEntityFlags(client) & FL_ONGROUND) + { + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + new Float:flSpeed = GetVectorLength(flVelocity); + + new Float:flPunchIdle[3]; + + if (flSpeed > 0.0) + { + if (flSpeed >= 60.0) + { + flPunchIdle[0] = Sine(GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_X / 400.0; + flPunchIdle[1] = Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Y / 400.0; + flPunchIdle[2] = Sine(1.6 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + + // Calculate roll. + decl Float:flForward[3], Float:flVelocityDirection[3]; + GetClientEyeAngles(client, flForward); + GetVectorAngles(flVelocity, flVelocityDirection); + + new Float:flYawDiff = AngleDiff(flForward[1], flVelocityDirection[1]); + if (FloatAbs(flYawDiff) > 90.0) flYawDiff = AngleDiff(flForward[1] + 180.0, flVelocityDirection[1]) * -1.0; + + new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); + new Float:flRollScalar = flSpeed / flWalkSpeed; + if (flRollScalar > 1.0) flRollScalar = 1.0; + + new Float:flRollScale = (flYawDiff / 90.0) * 0.25 * flRollScalar; + flPunchIdle[0] = 0.0; + flPunchIdle[1] = 0.0; + flPunchIdle[2] = flRollScale * -1.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + + /* + if (flSpeed < 60.0) + { + flPunchIdle[0] = FloatAbs(Cosine(GetGameTime() * 1.25) * 0.047); + flPunchIdle[1] = Sine(GetGameTime() * 1.25) * 0.075; + flPunchIdle[2] = 0.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + */ + } + } + + if (g_bPlayerViewbobHurtEnabled) + { + // Shake screen the more the player is hurt. + new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); + new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); + + decl Float:flPunchVelHurt[3]; + flPunchVelHurt[0] = Sine(1.22 * GetGameTime()) * 48.5 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + flPunchVelHurt[1] = Sine(2.12 * GetGameTime()) * 80.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + flPunchVelHurt[2] = Sine(0.5 * GetGameTime()) * 36.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + + AddVectors(flPunchVel, flPunchVelHurt, flPunchVel); + } + + ClientViewPunch(client, flPunchVel); + } + } + } +} + +public Action:Timer_ClientIncreaseStatic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; + + g_flPlayerStaticAmount[client] += 0.05; + if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; + + if (g_strPlayerStaticSound[client][0]) + { + EmitSoundToClient(client, g_strPlayerStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerStaticAmount[client]); + + if (g_flPlayerStaticAmount[client] >= 0.5) ClientAddStress(client, 0.03); + else + { + ClientAddStress(client, 0.02); + } + } + + return Plugin_Continue; +} + +public Action:Timer_ClientDecreaseStatic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; + + g_flPlayerStaticAmount[client] -= 0.05; + if (g_flPlayerStaticAmount[client] < 0.0) g_flPlayerStaticAmount[client] = 0.0; + + if (g_strPlayerLastStaticSound[client][0]) + { + new Float:flVolume = g_flPlayerStaticAmount[client]; + if (flVolume > 0.0) + { + EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); + } + } + + if (g_flPlayerStaticAmount[client] <= 0.0) + { + // I've done my job; no point to keep on doing it. + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + g_hPlayerStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_ClientFadeOutLastStaticSound(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerLastStaticTimer[client]) return Plugin_Stop; + + if (StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) + { + // Wait, the player's current static sound is the same one we're stopping. Abort! + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + if (g_strPlayerLastStaticSound[client][0]) + { + new Float:flDiff = (GetGameTime() - g_flPlayerLastStaticTime[client]) / 1.0; + if (flDiff > 1.0) flDiff = 1.0; + + new Float:flVolume = g_flPlayerLastStaticVolume[client] - flDiff; + if (flVolume < 0.0) flVolume = 0.0; + + if (flVolume <= 0.0) + { + // I've done my job; no point to keep on doing it. + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + else + { + EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); + } + } + else + { + // I've done my job; no point to keep on doing it. + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +// ========================================================== +// INTERACTIVE GLOW FUNCTIONS +// ========================================================== + +static ClientProcessInteractiveGlow(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client) || (g_bPlayerEliminated[client] && !g_bPlayerProxy[client]) || IsClientInGhostMode(client)) return; + + new iOldLookEntity = EntRefToEntIndex(g_iPlayerInteractiveGlowTargetEntity[client]); + + decl Float:flStartPos[3], Float:flMyEyeAng[3]; + GetClientEyePosition(client, flStartPos); + GetClientEyeAngles(client, flMyEyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flMyEyeAng, MASK_VISIBLE, RayType_Infinite, TraceRayDontHitPlayers, -1); + new iEnt = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (IsValidEntity(iEnt)) + { + g_iPlayerInteractiveGlowTargetEntity[client] = EntRefToEntIndex(iEnt); + } + else + { + g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; + } + + if (iEnt != iOldLookEntity) + { + ClientRemoveInteractiveGlow(client); + + if (IsEntityClassname(iEnt, "prop_dynamic", false)) + { + decl String:sTargetName[64]; + GetEntPropString(iEnt, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); + + if (StrContains(sTargetName, "sf2_page", false) == 0 || StrContains(sTargetName, "sf2_interact", false) == 0) + { + ClientCreateInteractiveGlow(client, iEnt); + } + } + } +} + +ClientResetInteractiveGlow(client) +{ + ClientRemoveInteractiveGlow(client); + g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; +} + +/** + * Removes the player's current interactive glow entity. + */ +ClientRemoveInteractiveGlow(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientRemoveInteractiveGlow(%d)", client); +#endif + + new ent = EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerInteractiveGlowEntity[client] = INVALID_ENT_REFERENCE; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientRemoveInteractiveGlow(%d)", client); +#endif +} + +/** + * Creates an interactive glow for an entity to show to a player. + */ +bool:ClientCreateInteractiveGlow(client, iEnt, const String:sAttachment[]="") +{ + ClientRemoveInteractiveGlow(client); + + if (!IsClientInGame(client)) return false; + + if (!iEnt || !IsValidEdict(iEnt)) return false; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientCreateInteractiveGlow(%d)", client); +#endif + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetEntPropString(iEnt, Prop_Data, "m_ModelName", sBuffer, sizeof(sBuffer)); + + if (strlen(sBuffer) == 0) + { + return false; + } + + new ent = CreateEntityByName("tf_taunt_prop"); + if (ent != -1) + { + g_iPlayerInteractiveGlowEntity[client] = EntIndexToEntRef(ent); + + new Float:flModelScale = GetEntPropFloat(iEnt, Prop_Send, "m_flModelScale"); + + SetEntityModel(ent, sBuffer); + DispatchSpawn(ent); + ActivateEntity(ent); + SetEntityRenderMode(ent, RENDER_TRANSCOLOR); + SetEntityRenderColor(ent, 0, 0, 0, 0); + SetEntProp(ent, Prop_Send, "m_bGlowEnabled", 1); + SetEntPropFloat(ent, Prop_Send, "m_flModelScale", flModelScale); + + new iFlags = GetEntProp(ent, Prop_Send, "m_fEffects"); + SetEntProp(ent, Prop_Send, "m_fEffects", iFlags | (1 << 0)); + + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", iEnt); + + if (sAttachment[0]) + { + SetVariantString(sAttachment); + AcceptEntityInput(ent, "SetParentAttachment"); + } + + SDKHook(ent, SDKHook_SetTransmit, Hook_InterativeGlowSetTransmit); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> true", client); +#endif + + return true; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> false", client); +#endif + + return false; +} + +public Action:Hook_InterativeGlowSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[other]) != ent) return Plugin_Handled; + + return Plugin_Continue; +} + +// ========================================================== +// BREATHING FUNCTIONS +// ========================================================== + +ClientResetBreathing(client) +{ + g_bPlayerBreath[client] = false; + g_hPlayerBreathTimer[client] = INVALID_HANDLE; +} + +Float:ClientCalculateBreathingCooldown(client) +{ + new Float:flAverage = 0.0; + new iAverageNum = 0; + + // Sprinting only, for now. + flAverage += (SF2_PLAYER_BREATH_COOLDOWN_MAX * 6.7765 * Pow((float(g_iPlayerSprintPoints[client]) / 100.0), 1.65)); + iAverageNum++; + + flAverage /= float(iAverageNum) + + if (flAverage < SF2_PLAYER_BREATH_COOLDOWN_MIN) flAverage = SF2_PLAYER_BREATH_COOLDOWN_MIN; + + return flAverage; +} + +ClientStartBreathing(client) +{ + g_bPlayerBreath[client] = true; + g_hPlayerBreathTimer[client] = CreateTimer(ClientCalculateBreathingCooldown(client), Timer_ClientBreath, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStopBreathing(client) +{ + g_bPlayerBreath[client] = false; + g_hPlayerBreathTimer[client] = INVALID_HANDLE; +} + +bool:ClientCanBreath(client) +{ + return bool:(ClientCalculateBreathingCooldown(client) < SF2_PLAYER_BREATH_COOLDOWN_MAX); +} + +public Action:Timer_ClientBreath(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerBreathTimer[client]) return; + + if (!g_bPlayerBreath[client]) return; + + if (ClientCanBreath(client)) + { + EmitSoundToAll(g_strPlayerBreathSounds[GetRandomInt(0, sizeof(g_strPlayerBreathSounds) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + + ClientStartBreathing(client); + return; + } + + ClientStopBreathing(client); +} + +// ========================================================== +// SPRINTING FUNCTIONS +// ========================================================== + +bool:IsClientSprinting(client) +{ + return g_bPlayerSprint[client]; +} + +ClientGetSprintPoints(client) +{ + return g_iPlayerSprintPoints[client]; +} + +ClientResetSprint(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSprint(%d)", client); +#endif + + g_bPlayerSprint[client] = false; + g_iPlayerSprintPoints[client] = 100; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + + if (IsValidClient(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + + ClientSetFOV(client, g_iPlayerDesiredFOV[client]); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSprint(%d)", client); +#endif +} + +ClientStartSprint(client) +{ + if (IsClientSprinting(client)) return; + + g_bPlayerSprint[client] = true; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + ClientSprintTimer(client); + TriggerTimer(g_hPlayerSprintTimer[client], true); + + SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); +} + +static ClientSprintTimer(client, bool:bRecharge=false) +{ + new Float:flRate = 0.28; + if (bRecharge) flRate = 0.8; + + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + + if (bRecharge) + { + if (!(GetEntityFlags(client) & FL_ONGROUND)) flRate *= 0.75; + else if (GetVectorLength(flVelocity) == 0.0) + { + if (GetEntProp(client, Prop_Send, "m_bDucked")) flRate *= 0.66; + else flRate *= 0.75; + } + } + else + { + if (TF2_GetPlayerClass(client) == TFClass_Scout) flRate *= 1.15; + } + + if (bRecharge) g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientRechargeSprint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + else g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientSprinting, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStopSprint(client) +{ + if (!IsClientSprinting(client)) return; + + g_bPlayerSprint[client] = false; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + ClientSprintTimer(client, true); + + SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); +} + +bool:IsClientReallySprinting(client) +{ + if (!IsClientSprinting(client)) return false; + if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; + + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + if (GetVectorLength(flVelocity) < 30.0) return false; + + return true; +} + +public Action:Timer_ClientSprinting(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSprintTimer[client]) return; + + if (!IsClientSprinting(client)) return; + + if (g_iPlayerSprintPoints[client] <= 0) + { + ClientStopSprint(client); + g_iPlayerSprintPoints[client] = 0; + return; + } + + if (IsClientReallySprinting(client)) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); + if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) + { + g_iPlayerSprintPoints[client]--; + } + } + + ClientSprintTimer(client); +} + +public Hook_ClientSprintingPreThink(client) +{ + if (!IsClientReallySprinting(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + return; + } + + new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); + + new iTargetFOV = g_iPlayerDesiredFOV[client] + 10; + + if (iFOV < iTargetFOV) + { + new iDiff = RoundFloat(FloatAbs(float(iFOV - iTargetFOV))); + if (iDiff >= 1) + { + ClientSetFOV(client, iFOV + 1); + } + else + { + ClientSetFOV(client, iTargetFOV); + } + } + else if (iFOV >= iTargetFOV) + { + ClientSetFOV(client, iTargetFOV); + //SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + } +} + +public Hook_ClientRechargeSprintPreThink(client) +{ + if (IsClientReallySprinting(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + return; + } + + new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); + if (iFOV > g_iPlayerDesiredFOV[client]) + { + new iDiff = RoundFloat(FloatAbs(float(iFOV - g_iPlayerDesiredFOV[client]))); + if (iDiff >= 1) + { + ClientSetFOV(client, iFOV - 1); + } + else + { + ClientSetFOV(client, g_iPlayerDesiredFOV[client]); + } + } + else if (iFOV <= g_iPlayerDesiredFOV[client]) + { + ClientSetFOV(client, g_iPlayerDesiredFOV[client]); + } +} + +public Action:Timer_ClientRechargeSprint(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSprintTimer[client]) return; + + if (IsClientSprinting(client)) + { + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + return; + } + + if (g_iPlayerSprintPoints[client] >= 100) + { + g_iPlayerSprintPoints[client] = 100; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + return; + } + + g_iPlayerSprintPoints[client]++; + ClientSprintTimer(client, true); +} + +// ========================================================== +// PROXY / GHOST AND GLOW FUNCTIONS +// ========================================================== + +ClientResetProxy(client, bool:bResetFull=true) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetProxy(%d)", client); +#endif + + new iOldMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + new String:sOldProfileName[SF2_MAX_PROFILE_NAME_LENGTH]; + if (iOldMaster >= 0) + { + NPCGetProfile(iOldMaster, sOldProfileName, sizeof(sOldProfileName)); + } + + new bool:bOldProxy = g_bPlayerProxy[client]; + if (bResetFull) + { + g_bPlayerProxy[client] = false; + g_iPlayerProxyMaster[client] = -1; + } + + g_iPlayerProxyControl[client] = 0; + g_hPlayerProxyControlTimer[client] = INVALID_HANDLE; + g_flPlayerProxyControlRate[client] = 0.0; + g_flPlayerProxyVoiceTimer[client] = INVALID_HANDLE; + + if (IsClientInGame(client)) + { + if (bOldProxy) + { + ClientStartProxyAvailableTimer(client); + + if (bResetFull) + { + SetVariantString(""); + AcceptEntityInput(client, "SetCustomModel"); + } + + if (sOldProfileName[0]) + { + ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_spawn", GetProfileNum(sOldProfileName, "sound_proxy_spawn_channel", SNDCHAN_AUTO)); + ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_hurt", GetProfileNum(sOldProfileName, "sound_proxy_hurt_channel", SNDCHAN_AUTO)); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetProxy(%d)", client); +#endif +} + +ClientStartProxyAvailableTimer(client) +{ + g_bPlayerProxyAvailable[client] = false; + g_hPlayerProxyAvailableTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerProxyWaitTime), Timer_ClientProxyAvailable, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStartProxyForce(client, iSlenderID, const Float:flPos[3]) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); +#endif + + g_iPlayerProxyAskMaster[client] = iSlenderID; + for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; + + g_iPlayerProxyAvailableCount[client] = 0; + g_bPlayerProxyAvailableInForce[client] = true; + g_hPlayerProxyAvailableTimer[client] = CreateTimer(1.0, Timer_ClientForceProxy, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerProxyAvailableTimer[client], true); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); +#endif +} + +ClientStopProxyForce(client) +{ + g_iPlayerProxyAvailableCount[client] = 0; + g_bPlayerProxyAvailableInForce[client] = false; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; +} + +public Action:Timer_ClientForceProxy(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerProxyAvailableTimer[client]) return Plugin_Stop; + + if (!IsRoundEnding()) + { + new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[client]); + if (iBossIndex != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumProxies = 0; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; + + iNumProxies++; + } + + if (iNumProxies < iMaxProxies) + { + if (g_iPlayerProxyAvailableCount[client] > 0) + { + g_iPlayerProxyAvailableCount[client]--; + + SetHudTextParams(-1.0, 0.25, + 1.0, + 255, 255, 255, 255, + _, + _, + 0.25, 1.25); + + ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Proxy Force Message", client, g_iPlayerProxyAvailableCount[client]); + + return Plugin_Continue; + } + else + { + ClientEnableProxy(client, iBossIndex); + TeleportEntity(client, g_iPlayerProxyAskPosition[client], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + } + } + else + { + PrintToChat(client, "%T", "SF2 Too Many Proxies", client); + } + } + } + + ClientStopProxyForce(client); + return Plugin_Stop; +} + +DisplayProxyAskMenu(client, iAskMaster, const Float:flPos[3]) +{ + decl String:sBuffer[512]; + new Handle:hMenu = CreateMenu(Menu_ProxyAsk); + SetMenuTitle(hMenu, "%T\n \n%T\n \n", "SF2 Proxy Ask Menu Title", client, "SF2 Proxy Ask Menu Description", client); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, "1", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + + g_iPlayerProxyAskMaster[client] = iAskMaster; + for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; + DisplayMenu(hMenu, client, 15); +} + +public Menu_ProxyAsk(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_End: CloseHandle(menu); + case MenuAction_Select: + { + if (!IsRoundEnding()) + { + new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[param1]); + if (iBossIndex != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumProxies; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; + + iNumProxies++; + } + + if (iNumProxies < iMaxProxies) + { + if (param2 == 0) + { + ClientEnableProxy(param1, iBossIndex); + TeleportEntity(param1, g_iPlayerProxyAskPosition[param1], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + } + else + { + ClientStartProxyAvailableTimer(param1); + } + } + else + { + PrintToChat(param1, "%T", "SF2 Too Many Proxies", param1); + } + } + } + } + } +} + +public Action:Timer_ClientProxyAvailable(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerProxyAvailableTimer[client]) return; + + g_bPlayerProxyAvailable[client] = true; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; +} + +ClientEnableProxy(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) return; + if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) return; + if (g_bPlayerProxy[client]) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + PvP_SetPlayerPvPState(client, false, false, false); + + ClientSetGhostModeState(client, false); + + ClientStopProxyForce(client); + + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + if (!IsPlayerAlive(client)) TF2_RespawnPlayer(client); + // Speed recalculation. Props to the creators of FF2/VSH for this snippet. + TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); + + g_bPlayerProxy[client] = true; + g_iPlayerProxyMaster[client] = NPCGetUniqueID(iBossIndex); + g_iPlayerProxyControl[client] = 100; + g_flPlayerProxyControlRate[client] = GetProfileFloat(sProfile, "proxies_controldrainrate"); + g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + g_bPlayerProxyAvailable[client] = false; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; + + decl String:sAllowedClasses[512]; + GetProfileString(sProfile, "proxies_classes", sAllowedClasses, sizeof(sAllowedClasses)); + + decl String:sClassName[64]; + TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); + if (sAllowedClasses[0] && sClassName[0] && StrContains(sAllowedClasses, sClassName, false) == -1) + { + // Pick the first class that's allowed. + new String:sAllowedClassesList[32][32]; + new iClassCount = ExplodeString(sAllowedClasses, " ", sAllowedClassesList, 32, 32); + if (iClassCount) + { + TF2_SetPlayerClass(client, TF2_GetClass(sAllowedClassesList[0]), _, false); + + new iMaxHealth = GetEntProp(client, Prop_Send, "m_iHealth"); + TF2_RegeneratePlayer(client); + SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); + SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); + } + } + + UTIL_ScreenFade(client, 200, 1, FFADE_IN, 255, 255, 255, 100); + PrecacheSound("weapons/teleporter_send.wav"); + EmitSoundToClient(client, "weapons/teleporter_send.wav", _, SNDCHAN_STATIC); + + ClientActivateUltravision(client); + + CreateTimer(0.33, Timer_ApplyCustomModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + Call_StartForward(fOnClientSpawnedAsProxy); + Call_PushCell(client); + Call_Finish(); +} + +public Action:Timer_ClientProxyControl(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerProxyControlTimer[client]) return; + + g_iPlayerProxyControl[client]--; + if (g_iPlayerProxyControl[client] <= 0) + { + // ForcePlayerSuicide isn't really dependable, since the player doesn't suicide until several seconds after spawning has passed. + SDKHooks_TakeDamage(client, client, client, 9001.0, DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + return; + } + + g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, userid, TIMER_FLAG_NO_MAPCHANGE); +} + +bool:DoesClientHaveConstantGlow(client) +{ + return g_bPlayerConstantGlowEnabled[client]; +} + +ClientDisableConstantGlow(client) +{ + if (!DoesClientHaveConstantGlow(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientDisableConstantGlow(%d)", client); +#endif + + g_bPlayerConstantGlowEnabled[client] = false; + + new iGlow = EntRefToEntIndex(g_iPlayerConstantGlowEntity[client]); + if (iGlow && iGlow != INVALID_ENT_REFERENCE) AcceptEntityInput(iGlow, "Kill"); + + g_iPlayerConstantGlowEntity[client] = INVALID_ENT_REFERENCE; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientDisableConstantGlow(%d)", client); +#endif +} + +bool:ClientEnableConstantGlow(client, const String:sAttachment[]="") +{ + if (DoesClientHaveConstantGlow(client)) return true; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientEnableConstantGlow(%d)", client); +#endif + + decl String:sModel[PLATFORM_MAX_PATH]; + GetClientModel(client, sModel, sizeof(sModel)); + + if (strlen(sModel) == 0) + { + // For some reason the model couldn't be found, so no. + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false (no model specified)", client); +#endif + + return false; + } + + new iGlow = CreateEntityByName("tf_taunt_prop"); + if (iGlow != -1) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> created"); +#endif + + g_bPlayerConstantGlowEnabled[client] = true; + g_iPlayerConstantGlowEntity[client] = EntIndexToEntRef(iGlow); + + new Float:flModelScale = GetEntPropFloat(client, Prop_Send, "m_flModelScale"); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) + { + DebugMessage("tf_taunt_prop -> get model and model scale (%s, %f, player class: %d)", sModel, flModelScale, TF2_GetPlayerClass(client)); + } +#endif + + SetEntityModel(iGlow, sModel); + DispatchSpawn(iGlow); + ActivateEntity(iGlow); + SetEntityRenderMode(iGlow, RENDER_TRANSCOLOR); + SetEntityRenderColor(iGlow, 0, 0, 0, 0); + SetEntProp(iGlow, Prop_Send, "m_bGlowEnabled", 1); + SetEntPropFloat(iGlow, Prop_Send, "m_flModelScale", flModelScale); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set model and model scale"); +#endif + + // Set effect flags. + new iFlags = GetEntProp(iGlow, Prop_Send, "m_fEffects"); + SetEntProp(iGlow, Prop_Send, "m_fEffects", iFlags | (1 << 0)); // EF_BONEMERGE + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set bonemerge flags"); +#endif + + SetVariantString("!activator"); + AcceptEntityInput(iGlow, "SetParent", client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent to client"); +#endif + + if (sAttachment[0]) + { + SetVariantString(sAttachment); + AcceptEntityInput(iGlow, "SetParentAttachment"); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent attachment to %s", sAttachment); +#endif + + SDKHook(iGlow, SDKHook_SetTransmit, Hook_ConstantGlowSetTransmit); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> true", client); +#endif + + return true; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false", client); +#endif + + return false; +} + +ClientResetJumpScare(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetJumpScare(%d)", client); +#endif + + g_iPlayerJumpScareBoss[client] = -1; + g_flPlayerJumpScareLifeTime[client] = -1.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetJumpScare(%d)", client); +#endif +} + +ClientDoJumpScare(client, iBossIndex, Float:flLifeTime) +{ + g_iPlayerJumpScareBoss[client] = NPCGetUniqueID(iBossIndex); + g_flPlayerJumpScareLifeTime[client] = GetGameTime() + flLifeTime; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_jumpscare", sBuffer, sizeof(sBuffer), 1); + + if (strlen(sBuffer) > 0) + { + EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN); + } +} + + /** + * Handles sprinting upon player input. + */ +ClientHandleSprint(client, bool:bSprint) +{ + if (!IsPlayerAlive(client) || + g_bPlayerEliminated[client] || + DidClientEscape(client) || + g_bPlayerProxy[client] || + IsClientInGhostMode(client)) return; + + if (bSprint) + { + if (g_iPlayerSprintPoints[client] > 0) + { + ClientStartSprint(client); + } + else + { + EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); + } + } + else + { + if (IsClientSprinting(client)) + { + ClientStopSprint(client); + } + } +} + +ClientOnButtonPress(client, button) +{ + switch (button) + { + case IN_ATTACK2: + { + if (IsPlayerAlive(client)) + { + if (!IsRoundInWarmup() && + !IsRoundInIntro() && + !IsRoundEnding() && + !DidClientEscape(client)) + { + if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) + { + ClientHandleFlashlight(client); + } + } + } + } + case IN_ATTACK3: + { + ClientHandleSprint(client, true); + } + case IN_RELOAD: + { + if (IsPlayerAlive(client)) + { + if (!g_bPlayerEliminated[client]) + { + if (!IsRoundEnding() && + !IsRoundInWarmup() && + !IsRoundInIntro() && + !DidClientEscape(client)) + { + ClientBlink(client); + } + } + } + } + case IN_JUMP: + { + if (IsPlayerAlive(client) && !(GetEntityFlags(client) & FL_FROZEN)) + { + if (!bool:GetEntProp(client, Prop_Send, "m_bDucked") && + (GetEntityFlags(client) & FL_ONGROUND) && + GetEntProp(client, Prop_Send, "m_nWaterLevel") < 2) + { + ClientOnJump(client); + } + } + } + } +} + +ClientOnButtonRelease(client, button) +{ + switch (button) + { + case IN_ATTACK3: + { + ClientHandleSprint(client, false); + } + } +} + +ClientOnJump(client) +{ + if (!g_bPlayerEliminated[client]) + { + if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); + if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) + { + g_iPlayerSprintPoints[client] -= 7; + if (g_iPlayerSprintPoints[client] < 0) g_iPlayerSprintPoints[client] = 0; + } + + if (!IsClientSprinting(client)) + { + if (g_hPlayerSprintTimer[client] == INVALID_HANDLE) + { + // If the player hasn't sprinted recently, force us to regenerate the stamina. + ClientSprintTimer(client, true); + } + } + } + } +} + +// ========================================================== +// DEATH CAM FUNCTIONS +// ========================================================== + +bool:IsClientInDeathCam(client) +{ + return g_bPlayerDeathCam[client]; +} + +public Action:Hook_DeathCamSetTransmit(slender, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerDeathCamEnt2[other]) != slender) return Plugin_Handled; + return Plugin_Continue; +} + +ClientResetDeathCam(client) +{ + if (!IsClientInDeathCam(client)) return; // no really need to reset if it wasn't set. + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetDeathCam(%d)", client); +#endif + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + + g_iPlayerDeathCamBoss[client] = -1; + g_bPlayerDeathCam[client] = false; + g_bPlayerDeathCamShowOverlay[client] = false; + g_hPlayerDeathCamTimer[client] = INVALID_HANDLE; + + new ent = EntRefToEntIndex(g_iPlayerDeathCamEnt[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Disable"); + AcceptEntityInput(ent, "Kill"); + } + + ent = EntRefToEntIndex(g_iPlayerDeathCamEnt2[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerDeathCamEnt[client] = INVALID_ENT_REFERENCE; + g_iPlayerDeathCamEnt2[client] = INVALID_ENT_REFERENCE; + + if (IsClientInGame(client)) + { + SetClientViewEntity(client, client); + } + + Call_StartForward(fOnClientEndDeathCam); + Call_PushCell(client); + Call_PushCell(iDeathCamBoss); + Call_Finish(); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetDeathCam(%d)", client); +#endif +} + +ClientStartDeathCam(client, iBossIndex, const Float:vecLookPos[3]) +{ + if (IsClientInDeathCam(client)) return; + if (!NPCIsValid(iBossIndex)) return; + + decl String:buffer[PLATFORM_MAX_PATH]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (GetProfileNum(sProfile, "death_cam_play_scare_sound")) + { + GetRandomStringFromProfile(sProfile, "sound_scare_player", buffer, sizeof(buffer)); + if (buffer[0]) EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + + GetRandomStringFromProfile(sProfile, "sound_player_deathcam", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + else + { + // Legacy support for "sound_player_death" + GetRandomStringFromProfile(sProfile, "sound_player_death", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + + GetRandomStringFromProfile(sProfile, "sound_player_deathcam_all", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + else + { + // Legacy support for "sound_player_death_all" + GetRandomStringFromProfile(sProfile, "sound_player_death_all", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + + // Call our forward. + Call_StartForward(fOnClientCaughtByBoss); + Call_PushCell(client); + Call_PushCell(iBossIndex); + Call_Finish(); + + if (!NPCHasDeathCamEnabled(iBossIndex)) + { + SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol changes our lifestate. + + // TODO: Add more attributes! + if (NPCHasAttribute(iBossIndex, "ignite player on death")) + { + new Float:flValue = NPCGetAttributeValue(iBossIndex, "ignite player on death"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + + SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + return; + } + + g_iPlayerDeathCamBoss[client] = NPCGetUniqueID(iBossIndex); + g_bPlayerDeathCam[client] = true; + g_bPlayerDeathCamShowOverlay[client] = false; + + decl Float:eyePos[3], Float:eyeAng[3], Float:vecAng[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + SubtractVectors(eyePos, vecLookPos, vecAng); + GetVectorAngles(vecAng, vecAng); + vecAng[0] = 0.0; + vecAng[2] = 0.0; + + // Create fake model. + new slender = SpawnSlenderModel(iBossIndex, vecLookPos); + TeleportEntity(slender, vecLookPos, vecAng, NULL_VECTOR); + g_iPlayerDeathCamEnt2[client] = EntIndexToEntRef(slender); + SDKHook(slender, SDKHook_SetTransmit, Hook_DeathCamSetTransmit); + + // Create camera look point. + decl String:sName[64]; + Format(sName, sizeof(sName), "sf2_boss_%d", EntIndexToEntRef(slender)); + + decl Float:flOffsetPos[3]; + new target = CreateEntityByName("info_target"); + GetProfileVector(sProfile, "death_cam_pos", flOffsetPos); + AddVectors(vecLookPos, flOffsetPos, flOffsetPos); + TeleportEntity(target, flOffsetPos, NULL_VECTOR, NULL_VECTOR); + DispatchKeyValue(target, "targetname", sName); + SetVariantString("!activator"); + AcceptEntityInput(target, "SetParent", slender); + + // Create the camera itself. + new camera = CreateEntityByName("point_viewcontrol"); + TeleportEntity(camera, eyePos, eyeAng, NULL_VECTOR); + DispatchKeyValue(camera, "spawnflags", "12"); + DispatchKeyValue(camera, "target", sName); + DispatchSpawn(camera); + AcceptEntityInput(camera, "Enable", client); + g_iPlayerDeathCamEnt[client] = EntIndexToEntRef(camera); + + if (GetProfileNum(sProfile, "death_cam_overlay") && GetProfileFloat(sProfile, "death_cam_time_overlay_start") >= 0.0) + { + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_overlay_start"), Timer_ClientResetDeathCam1, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + + Call_StartForward(fOnClientStartDeathCam); + Call_PushCell(client); + Call_PushCell(iBossIndex); + Call_Finish(); +} + +public Action:Timer_ClientResetDeathCam1(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerDeathCamTimer[client]) return; + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); + + g_bPlayerDeathCamShowOverlay[client] = true; + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, userid, TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ClientResetDeathCamEnd(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerDeathCamTimer[client]) return; + + SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol entity changes our damage state. + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + if (iDeathCamBoss != -1) + { + if (NPCHasAttribute(iDeathCamBoss, "ignite player on death")) + { + new Float:flValue = NPCGetAttributeValue(iDeathCamBoss, "ignite player on death"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + } + + SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + ClientResetDeathCam(client); +} + +// ========================================================== +// GHOST MODE FUNCTIONS +// ========================================================== + +static bool:g_bPlayerGhostMode[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerGhostModeTarget[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Handle:g_hPlayerGhostModeConnectionCheckTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerGhostModeConnectionTimeOutTime[MAXPLAYERS + 1] = { -1.0, ... }; +static Float:g_flPlayerGhostModeConnectionBootTime[MAXPLAYERS + 1] = { -1.0, ... }; + +/** + * Enables/Disables ghost mode on the player. + */ +ClientSetGhostModeState(client, bool:bState) +{ + if (bState == g_bPlayerGhostMode[client]) return; + + if (bState && !IsClientInGame(client)) return; + + g_bPlayerGhostMode[client] = bState; + g_iPlayerGhostModeTarget[client] = INVALID_ENT_REFERENCE; + + if (bState) + { + ClientHandleGhostMode(client, true); + + if (GetConVarBool(g_cvGhostModeConnectionCheck)) + { + g_hPlayerGhostModeConnectionCheckTimer[client] = CreateTimer(0.0, Timer_GhostModeConnectionCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + } + + PvP_OnClientGhostModeEnable(client); + } + else + { + g_hPlayerGhostModeConnectionCheckTimer[client] = INVALID_HANDLE; + g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + + if (IsClientInGame(client)) + { + TF2_RemoveCondition(client, TFCond_HalloweenGhostMode); + SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_PLAYER); + } + } +} + +public Action:Timer_GhostModeConnectionCheck(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerGhostModeConnectionCheckTimer[client]) return Plugin_Stop; + + if (!IsFakeClient(client) && IsClientTimingOut(client)) + { + new Float:bootTime = g_flPlayerGhostModeConnectionBootTime[client]; + if (bootTime < 0.0) + { + bootTime = GetGameTime() + GetConVarFloat(g_cvGhostModeConnectionTolerance); + g_flPlayerGhostModeConnectionBootTime[client] = bootTime; + g_flPlayerGhostModeConnectionTimeOutTime[client] = GetGameTime(); + } + + if (GetGameTime() >= bootTime) + { + ClientSetGhostModeState(client, false); + TF2_RespawnPlayer(client); + + decl String:authString[128]; + GetClientAuthString(client, authString, sizeof(authString)); + + LogSF2Message("Removed %N (%s) from ghost mode due to timing out for %f seconds", client, authString, GetConVarFloat(g_cvGhostModeConnectionTolerance)); + + new Float:timeOutTime = g_flPlayerGhostModeConnectionTimeOutTime[client]; + CPrintToChat(client, "%T", "SF2 Ghost Mode Bad Connection", client, RoundFloat(bootTime - timeOutTime)); + + return Plugin_Stop; + } + } + else + { + // Player regained connection; reset. + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + } + + return Plugin_Continue; +} + +/** + * Makes sure that the player is a ghost when ghost mode is activated. + */ +ClientHandleGhostMode(client, bool:bForceSpawn=false) +{ + if (!IsClientInGhostMode(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientHandleGhostMode(%d, %d)", client, bForceSpawn); +#endif + + if (!TF2_IsPlayerInCondition(client, TFCond_HalloweenGhostMode) || bForceSpawn) + { + TF2_AddCondition(client, TFCond_HalloweenGhostMode, -1.0); + SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_DEBRIS); + + // Set first observer target. + ClientGhostModeNextTarget(client); + ClientActivateUltravision(client); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientHandleGhostMode(%d, %d)", client, bForceSpawn); +#endif +} + +ClientGhostModeNextTarget(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientGhostModeNextTarget(%d)", client); +#endif + + new iLastTarget = EntRefToEntIndex(g_iPlayerGhostModeTarget[client]); + new iNextTarget = -1; + new iFirstTarget = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && (!g_bPlayerEliminated[i] || g_bPlayerProxy[i]) && !IsClientInGhostMode(i) && !DidClientEscape(i) && IsPlayerAlive(i)) + { + if (iFirstTarget == -1) iFirstTarget = i; + if (i > iLastTarget) + { + iNextTarget = i; + break; + } + } + } + + new iTarget = -1; + if (IsValidClient(iNextTarget)) iTarget = iNextTarget; + else iTarget = iFirstTarget; + + if (IsValidClient(iTarget)) + { + g_iPlayerGhostModeTarget[client] = EntIndexToEntRef(iTarget); + + decl Float:flPos[3], Float:flAng[3], Float:flVelocity[3]; + GetClientAbsOrigin(iTarget, flPos); + GetClientEyeAngles(iTarget, flAng); + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsVelocity", flVelocity); + TeleportEntity(client, flPos, flAng, flVelocity); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientGhostModeNextTarget(%d)", client); +#endif +} + +bool:IsClientInGhostMode(client) +{ + return g_bPlayerGhostMode[client]; +} + +// ========================================================== +// SCARE FUNCTIONS +// ========================================================== + +ClientPerformScare(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) + { + LogError("Could not perform scare on client %d: boss does not exist!", client); + return; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + g_flPlayerScareLastTime[client][iBossIndex] = GetGameTime(); + g_flPlayerScareNextTime[client][iBossIndex] = GetGameTime() + NPCGetScareCooldown(iBossIndex); + + // See how much Sanity should be drained from a scare. + new Float:flStaticAmount = GetProfileFloat(sProfile, "scare_static_amount", 0.0); + g_flPlayerStaticAmount[client] += flStaticAmount; + if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; + + decl String:sScareSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_scare_player", sScareSound, sizeof(sScareSound)); + + if (sScareSound[0]) + { + EmitSoundToClient(client, sScareSound, _, MUSIC_CHAN, SNDLEVEL_NONE); + + if (NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS) + { + new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); + new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); + + g_flPlayerSightSoundNextTime[client][iBossIndex] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); + } + + if (g_flPlayerStress[client] > 0.4) + { + ClientAddStress(client, 0.4); + } + else + { + ClientAddStress(client, 0.66); + } + } + else + { + if (g_flPlayerStress[client] > 0.4) + { + ClientAddStress(client, 0.3); + } + else + { + ClientAddStress(client, 0.45); + } + } +} + +ClientPerformSightSound(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) + { + LogError("Could not perform sight sound on client %d: boss does not exist!", client); + return; + } + + if (!(NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS)) return; + + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); + if (iMaster == -1) iMaster = iBossIndex; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sSightSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_sight", sSightSound, sizeof(sSightSound)); + + if (sSightSound[0]) + { + EmitSoundToClient(client, sSightSound, _, MUSIC_CHAN, SNDLEVEL_NONE); + + new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); + new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); + + g_flPlayerSightSoundNextTime[client][iMaster] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); + + decl Float:flBossPos[3], Float:flMyPos[3]; + new iBoss = NPCGetEntIndex(iBossIndex); + GetClientAbsOrigin(client, flMyPos); + GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flBossPos); + new Float:flDistUnComfortZone = 400.0; + new Float:flBossDist = GetVectorDistance(flMyPos, flBossPos); + + new Float:flStressScalar = 1.0 + (flDistUnComfortZone / flBossDist); + + ClientAddStress(client, 0.1 * flStressScalar); + } + else + { + LogError("Warning! %s supports sight sounds, but was given a blank sound!", sProfile); + } +} + +ClientResetScare(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetScare(%d)", client); +#endif + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_flPlayerScareNextTime[client][i] = -1.0; + g_flPlayerScareLastTime[client][i] = -1.0; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetScare(%d)", client); +#endif +} + +// ========================================================== +// ANTI-CAMPING FUNCTIONS +// ========================================================== + +stock ClientResetCampingStats(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetCampingStats(%d)", client); +#endif + + g_iPlayerCampingStrikes[client] = 0; + g_hPlayerCampingTimer[client] = INVALID_HANDLE; + g_bPlayerCampingFirstTime[client] = true; + g_flPlayerCampingLastPosition[client][0] = 0.0; + g_flPlayerCampingLastPosition[client][1] = 0.0; + g_flPlayerCampingLastPosition[client][2] = 0.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetCampingStats(%d)", client); +#endif +} + +ClientStartCampingTimer(client) +{ + g_hPlayerCampingTimer[client] = CreateTimer(5.0, Timer_ClientCheckCamp, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ClientCheckCamp(Handle:timer, any:userid) +{ + if (IsRoundInWarmup()) return Plugin_Stop; + + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerCampingTimer[client]) return Plugin_Stop; + + if (IsRoundEnding() || !IsPlayerAlive(client) || g_bPlayerEliminated[client] || DidClientEscape(client)) return Plugin_Stop; + + if (!g_bPlayerCampingFirstTime[client]) + { + decl Float:flPos[3], Float:flMaxs[3], Float:flMins[3]; + GetClientAbsOrigin(client, flPos); + GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); + GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); + + // Only do something if the player is NOT stuck. + new Float:flDistFromLastPosition = GetVectorDistance(g_flPlayerCampingLastPosition[client], flPos); + new Float:flDistFromClosestBoss = 9999999.0; + new iClosestBoss = -1; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new iSlender = NPCGetEntIndex(i); + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; + + decl Float:flSlenderPos[3]; + SlenderGetAbsOrigin(i, flSlenderPos); + + new Float:flDist = GetVectorDistance(flSlenderPos, flPos); + if (flDist < flDistFromClosestBoss) + { + iClosestBoss = i; + flDistFromClosestBoss = flDist; + } + } + + if (GetConVarBool(g_cvCampingEnabled) && + !g_bRoundGrace && + !IsSpaceOccupiedIgnorePlayers(flPos, flMins, flMaxs, client) && + g_flPlayerStaticAmount[client] <= GetConVarFloat(g_cvCampingNoStrikeSanity) && + (iClosestBoss == -1 || flDistFromClosestBoss >= GetConVarFloat(g_cvCampingNoStrikeBossDistance)) && + flDistFromLastPosition <= GetConVarFloat(g_cvCampingMinDistance)) + { + g_iPlayerCampingStrikes[client]++; + if (g_iPlayerCampingStrikes[client] < GetConVarInt(g_cvCampingMaxStrikes)) + { + if (g_iPlayerCampingStrikes[client] >= GetConVarInt(g_cvCampingStrikesWarn)) + { + CPrintToChat(client, "{red}%T", "SF2 Camping System Warning", client, (GetConVarInt(g_cvCampingMaxStrikes) - g_iPlayerCampingStrikes[client]) * 5); + } + } + else + { + g_iPlayerCampingStrikes[client] = 0; + ClientStartDeathCam(client, 0, flPos); + } + } + else + { + // Forgiveness. + if (g_iPlayerCampingStrikes[client] > 0) g_iPlayerCampingStrikes[client]--; + } + + g_flPlayerCampingLastPosition[client][0] = flPos[0]; + g_flPlayerCampingLastPosition[client][1] = flPos[1]; + g_flPlayerCampingLastPosition[client][2] = flPos[2]; + } + else + { + g_bPlayerCampingFirstTime[client] = false; + } + + return Plugin_Continue; +} + +// ========================================================== +// BLINK FUNCTIONS +// ========================================================== + +bool:IsClientBlinking(client) +{ + return g_bPlayerBlink[client]; +} + +Float:ClientGetBlinkMeter(client) +{ + return g_flPlayerBlinkMeter[client]; +} + +ClientGetBlinkCount(client) +{ + return g_iPlayerBlinkCount[client]; +} + +/** + * Resets all data on blinking. + */ +ClientResetBlink(client) +{ + g_hPlayerBlinkTimer[client] = INVALID_HANDLE; + g_bPlayerBlink[client] = false; + g_flPlayerBlinkMeter[client] = 1.0; + g_iPlayerBlinkCount[client] = 0; +} + +/** + * Sets the player into a blinking state and blinds the player + */ +ClientBlink(client) +{ + if (IsRoundInWarmup() || DidClientEscape(client)) return; + + if (IsClientBlinking(client)) return; + + g_bPlayerBlink[client] = true; + g_iPlayerBlinkCount[client]++; + g_flPlayerBlinkMeter[client] = 0.0; + g_hPlayerBlinkTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerBlinkHoldTime), Timer_BlinkTimer2, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + UTIL_ScreenFade(client, 100, RoundToFloor(GetConVarFloat(g_cvPlayerBlinkHoldTime) * 1000.0), FFADE_IN, 0, 0, 0, 255); + + Call_StartForward(fOnClientBlink); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Unsets the player from the blinking state. + */ +ClientUnblink(client) +{ + if (!IsClientBlinking(client)) return; + + g_bPlayerBlink[client] = false; + g_hPlayerBlinkTimer[client] = INVALID_HANDLE; + g_flPlayerBlinkMeter[client] = 1.0; +} + +ClientStartDrainingBlinkMeter(client) +{ + g_hPlayerBlinkTimer[client] = CreateTimer(ClientGetBlinkRate(client), Timer_BlinkTimer, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_BlinkTimer(Handle:timer, any:userid) +{ + if (IsRoundInWarmup()) return Plugin_Stop; + + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerBlinkTimer[client]) return Plugin_Stop; + + if (IsPlayerAlive(client) && !IsClientInDeathCam(client) && !g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !IsRoundEnding()) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteBlinkOverride); + if ((!g_bRoundInfiniteBlink && iOverride != 1) || iOverride == 0) + { + g_flPlayerBlinkMeter[client] -= 0.05; + } + + if (g_flPlayerBlinkMeter[client] <= 0.0) + { + ClientBlink(client); + return Plugin_Stop; + } + } + + return Plugin_Continue; +} + +public Action:Timer_BlinkTimer2(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerBlinkTimer[client]) return; + + ClientUnblink(client); + ClientStartDrainingBlinkMeter(client); +} + +Float:ClientGetBlinkRate(client) +{ + new Float:flValue = GetConVarFloat(g_cvPlayerBlinkRate); + if (GetEntProp(client, Prop_Send, "m_nWaterLevel") >= 3) + { + // Being underwater makes you blink faster, obviously. + flValue *= 0.75; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + if (g_bPlayerSeesSlender[client][i]) + { + flValue *= GetProfileFloat(sProfile, "blink_look_rate_multiply", 1.0); + } + + else if (g_iPlayerStaticMode[client][i] == Static_Increase) + { + flValue *= GetProfileFloat(sProfile, "blink_static_rate_multiply", 1.0); + } + } + + if (TF2_GetPlayerClass(client) == TFClass_Sniper) flValue *= 1.4; + + if (IsClientUsingFlashlight(client)) + { + decl Float:startPos[3], Float:endPos[3], Float:flDirection[3]; + new Float:flLength = SF2_FLASHLIGHT_LENGTH; + GetClientEyePosition(client, startPos); + GetClientEyePosition(client, endPos); + GetClientEyeAngles(client, flDirection); + GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flDirection, flDirection); + ScaleVector(flDirection, flLength); + AddVectors(endPos, flDirection, endPos); + new Handle:hTrace = TR_TraceRayFilterEx(startPos, endPos, MASK_VISIBLE, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); + TR_GetEndPosition(endPos, hTrace); + new bool:bHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bHit) + { + new Float:flPercent = (GetVectorDistance(startPos, endPos) / flLength); + flPercent *= 3.5; + if (flPercent > 1.0) flPercent = 1.0; + flValue *= flPercent; + } + } + + return flValue; +} + +// ========================================================== +// SCREEN OVERLAY FUNCTIONS +// ========================================================== + +ClientAddStress(client, Float:flStressAmount) +{ + g_flPlayerStress[client] += flStressAmount; + if (g_flPlayerStress[client] < 0.0) g_flPlayerStress[client] = 0.0; + if (g_flPlayerStress[client] > 1.0) g_flPlayerStress[client] = 1.0; + + //PrintCenterText(client, "g_flPlayerStress[%d] = %f", client, g_flPlayerStress[client]); + + SlenderOnClientStressUpdate(client); +} + +stock ClientResetOverlay(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetOverlay(%d)", client); +#endif + + g_hPlayerOverlayCheck[client] = INVALID_HANDLE; + + if (IsClientInGame(client)) + { + ClientCommand(client, "r_screenoverlay \"\""); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetOverlay(%d)", client); +#endif +} + +public Action:Timer_PlayerOverlayCheck(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerOverlayCheck[client]) return Plugin_Stop; + + if (IsRoundInWarmup()) return Plugin_Continue; + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + new iJumpScareBoss = NPCGetFromUniqueID(g_iPlayerJumpScareBoss[client]); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sMaterial[PLATFORM_MAX_PATH]; + + if (IsClientInDeathCam(client) && iDeathCamBoss != -1 && g_bPlayerDeathCamShowOverlay[client]) + { + NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); + GetRandomStringFromProfile(sProfile, "overlay_player_death", sMaterial, sizeof(sMaterial), 1); + } + else if (iJumpScareBoss != -1 && GetGameTime() <= g_flPlayerJumpScareLifeTime[client]) + { + NPCGetProfile(iJumpScareBoss, sProfile, sizeof(sProfile)); + GetRandomStringFromProfile(sProfile, "overlay_jumpscare", sMaterial, sizeof(sMaterial), 1); + } + else if (IsClientInGhostMode(client)) + { + strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_GHOST); + } + else if (IsRoundInWarmup() || g_bPlayerEliminated[client] || DidClientEscape(client) && !IsClientInGhostMode(client)) + { + return Plugin_Continue; + } + else + { + if (!g_iPlayerPreferences[client][PlayerPreference_FilmGrain]) + strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); + else + strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT); + } + + ClientCommand(client, "r_screenoverlay %s", sMaterial); + return Plugin_Continue; +} + +// ========================================================== +// MUSIC SYSTEM FUNCTIONS +// ========================================================== + +stock ClientUpdateMusicSystem(client, bool:bInitialize=false) +{ + new iOldPageMusicMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); + new iOldMusicFlags = g_iPlayerMusicFlags[client]; + new iChasingBoss = -1; + new iChasingSeeBoss = -1; + new iAlertBoss = -1; + new i20DollarsBoss = -1; + + if (IsRoundEnding() || !IsClientInGame(client) || IsFakeClient(client) || DidClientEscape(client) || (g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !g_bPlayerProxy[client])) + { + g_iPlayerMusicFlags[client] = 0; + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + } + else + { + new bool:bPlayMusicOnEscape = true; + decl String:sName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_escape_custommusic", false)) + { + bPlayMusicOnEscape = false; + break; + } + } + + // Page music first. + new iPageRange = 0; + + if (GetArraySize(g_hPageMusicRanges) > 0) // Map has its own defined page music? + { + for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) + { + ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + new iMin = GetArrayCell(g_hPageMusicRanges, i, 1); + new iMax = GetArrayCell(g_hPageMusicRanges, i, 2); + + if (g_iPageCount >= iMin && g_iPageCount <= iMax) + { + g_iPlayerPageMusicMaster[client] = GetArrayCell(g_hPageMusicRanges, i); + break; + } + } + } + else // Nope. Use old system instead. + { + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + + new Float:flPercent = g_iPageMax > 0 ? (float(g_iPageCount) / float(g_iPageMax)) : 0.0; + if (flPercent > 0.0 && flPercent <= 0.25) iPageRange = 1; + else if (flPercent > 0.25 && flPercent <= 0.5) iPageRange = 2; + else if (flPercent > 0.5 && flPercent <= 0.75) iPageRange = 3; + else if (flPercent > 0.75) iPageRange = 4; + + if (iPageRange == 1) ClientAddMusicFlag(client, MUSICF_PAGES1PERCENT); + else if (iPageRange == 2) ClientAddMusicFlag(client, MUSICF_PAGES25PERCENT); + else if (iPageRange == 3) ClientAddMusicFlag(client, MUSICF_PAGES50PERCENT); + else if (iPageRange == 4) ClientAddMusicFlag(client, MUSICF_PAGES75PERCENT); + } + + if (iPageRange != 1) ClientRemoveMusicFlag(client, MUSICF_PAGES1PERCENT); + if (iPageRange != 2) ClientRemoveMusicFlag(client, MUSICF_PAGES25PERCENT); + if (iPageRange != 3) ClientRemoveMusicFlag(client, MUSICF_PAGES50PERCENT); + if (iPageRange != 4) ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); + + if (IsRoundInEscapeObjective() && !bPlayMusicOnEscape) + { + ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + } + + new iOldChasingBoss = g_iPlayerChaseMusicMaster[client]; + new iOldChasingSeeBoss = g_iPlayerChaseMusicSeeMaster[client]; + new iOldAlertBoss = g_iPlayerAlertMusicMaster[client]; + new iOld20DollarsBoss = g_iPlayer20DollarsMusicMaster[client]; + + new Float:flAnger = -1.0; + new Float:flSeeAnger = -1.0; + new Float:flAlertAnger = -1.0; + new Float:fl20DollarsAnger = -1.0; + + decl Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetEntIndex(i) == INVALID_ENT_REFERENCE) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + new iBossType = NPCGetType(i); + + switch (iBossType) + { + case SF2BossType_Chaser: + { + GetClientAbsOrigin(client, flBuffer); + SlenderGetAbsOrigin(i, flBuffer3); + + new iTarget = EntRefToEntIndex(g_iSlenderTarget[i]); + if (iTarget != -1) + { + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBuffer2); + + if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK || g_iSlenderState[i] == STATE_STUN) && + !(NPCGetFlags(i) & SFF_MARKEDASFAKE) && + (iTarget == client || GetVectorDistance(flBuffer, flBuffer2) <= 850.0 || GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0)) + { + decl String:sPath[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sPath, sizeof(sPath), 1); + if (sPath[0]) + { + if (NPCGetAnger(i) > flAnger) + { + flAnger = NPCGetAnger(i); + iChasingBoss = i; + } + } + + if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK) && + PlayerCanSeeSlender(client, i, false)) + { + if (iOldChasingSeeBoss == -1 || !PlayerCanSeeSlender(client, iOldChasingSeeBoss, false) || (NPCGetAnger(i) > flSeeAnger)) + { + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sPath, sizeof(sPath), 1); + + if (sPath[0]) + { + flSeeAnger = NPCGetAnger(i); + iChasingSeeBoss = i; + } + } + + if (g_b20Dollars) + { + if (iOld20DollarsBoss == -1 || !PlayerCanSeeSlender(client, iOld20DollarsBoss, false) || (NPCGetAnger(i) > fl20DollarsAnger)) + { + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sPath, sizeof(sPath), 1); + + if (sPath[0]) + { + fl20DollarsAnger = NPCGetAnger(i); + i20DollarsBoss = i; + } + } + } + } + } + } + + if (g_iSlenderState[i] == STATE_ALERT) + { + decl String:sPath[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sPath, sizeof(sPath), 1); + if (!sPath[0]) continue; + + if (!(NPCGetFlags(i) & SFF_MARKEDASFAKE)) + { + if (GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0) + { + if (NPCGetAnger(i) > flAlertAnger) + { + flAlertAnger = NPCGetAnger(i); + iAlertBoss = i; + } + } + } + } + } + } + } + + if (iChasingBoss != iOldChasingBoss) + { + if (iChasingBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_CHASE); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_CHASE); + } + } + + if (iChasingSeeBoss != iOldChasingSeeBoss) + { + if (iChasingSeeBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_CHASEVISIBLE); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_CHASEVISIBLE); + } + } + + if (iAlertBoss != iOldAlertBoss) + { + if (iAlertBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_ALERT); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_ALERT); + } + } + + if (i20DollarsBoss != iOld20DollarsBoss) + { + if (i20DollarsBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_20DOLLARS); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_20DOLLARS); + } + } + } + + if (IsValidClient(client)) + { + new bool:bWasChase = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASE); + new bool:bChase = ClientHasMusicFlag(client, MUSICF_CHASE); + new bool:bWasChaseSee = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASEVISIBLE); + new bool:bChaseSee = ClientHasMusicFlag(client, MUSICF_CHASEVISIBLE); + new bool:bAlert = ClientHasMusicFlag(client, MUSICF_ALERT); + new bool:bWasAlert = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_ALERT); + new bool:b20Dollars = ClientHasMusicFlag(client, MUSICF_20DOLLARS); + new bool:bWas20Dollars = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_20DOLLARS); + + // Custom system. + if (GetArraySize(g_hPageMusicRanges) > 0) + { + decl String:sPath[PLATFORM_MAX_PATH]; + + new iMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); + if (iMaster != INVALID_ENT_REFERENCE) + { + for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) + { + new ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + + if (ent == iMaster && + (iOldPageMusicMaster != iMaster || iOldPageMusicMaster == INVALID_ENT_REFERENCE)) + { + if (!sPath[0]) + { + LogError("Could not play music of page range %d-%d: no sound path specified!", GetArrayCell(g_hPageMusicRanges, i, 1), GetArrayCell(g_hPageMusicRanges, i, 2)); + } + else + { + ClientMusicStart(client, sPath, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) + { + GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + StopSound(client, MUSIC_CHAN, sPath); + } + } + } + } + } + else + { + if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) + { + GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + StopSound(client, MUSIC_CHAN, sPath); + } + } + } + } + + // Old system. + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES1_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES1_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES2_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES2_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES3_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES3_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES4_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES4_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + new iMainMusicState = 0; + + if (bAlert != bWasAlert || iAlertBoss != g_iPlayerAlertMusicMaster[client]) + { + if (bAlert && !bChase) + { + ClientAlertMusicStart(client, iAlertBoss); + if (!bWasAlert) iMainMusicState = -1; + } + else + { + ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); + if (!bChase && bWasAlert) iMainMusicState = 1; + } + } + + if (bChase != bWasChase || iChasingBoss != g_iPlayerChaseMusicMaster[client]) + { + if (bChase) + { + ClientMusicChaseStart(client, iChasingBoss); + + if (!bWasChase) + { + iMainMusicState = -1; + + if (bAlert) + { + ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); + } + } + } + else + { + ClientMusicChaseStop(client, g_iPlayerChaseMusicMaster[client]); + if (bWasChase) + { + if (bAlert) + { + ClientAlertMusicStart(client, iAlertBoss); + } + else + { + iMainMusicState = 1; + } + } + } + } + + if (bChaseSee != bWasChaseSee || iChasingSeeBoss != g_iPlayerChaseMusicSeeMaster[client]) + { + if (bChaseSee) + { + ClientMusicChaseSeeStart(client, iChasingSeeBoss); + } + else + { + ClientMusicChaseSeeStop(client, g_iPlayerChaseMusicSeeMaster[client]); + } + } + + if (b20Dollars != bWas20Dollars || i20DollarsBoss != g_iPlayer20DollarsMusicMaster[client]) + { + if (b20Dollars) + { + Client20DollarsMusicStart(client, i20DollarsBoss); + } + else + { + Client20DollarsMusicStop(client, g_iPlayer20DollarsMusicMaster[client]); + } + } + + if (iMainMusicState == 1) + { + ClientMusicStart(client, g_strPlayerMusic[client], _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + else if (iMainMusicState == -1) + { + ClientMusicStop(client); + } + + if (bChase || bAlert) + { + new iBossToUse = -1; + if (bChase) + { + iBossToUse = iChasingBoss; + } + else + { + iBossToUse = iAlertBoss; + } + + if (iBossToUse != -1) + { + // We got some alert/chase music going on! The player's excitement will no doubt go up! + // Excitement, though, really depends on how close the boss is in relation to the + // player. + + new Float:flBossDist = NPCGetDistanceFromEntity(iBossToUse, client); + new Float:flScalar = flBossDist / 700.0 + if (flScalar > 1.0) flScalar = 1.0; + new Float:flStressAdd = 0.1 * (1.0 - flScalar); + + ClientAddStress(client, flStressAdd); + } + } + } +} + +stock ClientMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); + strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerMusicFlags[client] = 0; + g_flPlayerMusicVolume[client] = 0.0; + g_flPlayerMusicTargetVolume[client] = 0.0; + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; +} + +stock ClientMusicStart(client, const String:sNewMusic[], Float:flVolume=-1.0, Float:flTargetVolume=-1.0, bool:bCopyOnly=false) +{ + if (!IsValidClient(client)) return; + if (!sNewMusic[0]) return; + + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); + + if (!StrEqual(sOldMusic, sNewMusic, false)) + { + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + + strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), sNewMusic); + if (flVolume >= 0.0) g_flPlayerMusicVolume[client] = flVolume; + if (flTargetVolume >= 0.0) g_flPlayerMusicTargetVolume[client] = flTargetVolume; + + if (!bCopyOnly) + { + g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeInMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerMusicTimer[client], true); + } + else + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + } +} + +stock ClientMusicStop(client) +{ + g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeOutMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerMusicTimer[client], true); +} + +stock Client20DollarsMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayer20DollarsMusic[client]); + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayer20DollarsMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayer20DollarsMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayer20DollarsMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock Client20DollarsMusicStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayer20DollarsMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayer20DollarsMusicMaster[client] = iBossIndex; + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), sBuffer); + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeIn20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientAlertMusicStop(client, iOldMaster); + } +} + +stock Client20DollarsMusicStop(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayer20DollarsMusicMaster[client]) + { + g_iPlayer20DollarsMusicMaster[client] = -1; + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); + } + + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOut20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); +} + +stock ClientAlertMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerAlertMusic[client]); + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerAlertMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerAlertMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayerAlertMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_alert_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientAlertMusicStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayerAlertMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayerAlertMusicMaster[client] = iBossIndex; + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), sBuffer); + g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientAlertMusicStop(client, iOldMaster); + } +} + +stock ClientAlertMusicStop(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerAlertMusicMaster[client]) + { + g_iPlayerAlertMusicMaster[client] = -1; + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); + } + + g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); +} + +stock ClientChaseMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusic[client]); + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerChaseMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerChaseMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayerChaseMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_chase_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientMusicChaseStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayerChaseMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayerChaseMusicMaster[client] = iBossIndex; + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), sBuffer); + g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientMusicChaseStop(client, iOldMaster); + } +} + +stock ClientMusicChaseStop(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerChaseMusicMaster[client]) + { + g_iPlayerChaseMusicMaster[client] = -1; + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); + } + + g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); +} + +stock ClientChaseMusicSeeReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusicSee[client]); + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); + if (IsClientInGame(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerChaseMusicSeeMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerChaseMusicSeeTimer[client][i] = INVALID_HANDLE; + g_flPlayerChaseMusicSeeVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsClientInGame(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientMusicChaseSeeStart(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + + new iOldMaster = g_iPlayerChaseMusicSeeMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); + if (!sBuffer[0]) return; + + g_iPlayerChaseMusicSeeMaster[client] = iBossIndex; + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), sBuffer); + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientMusicChaseSeeStop(client, iOldMaster); + } +} + +stock ClientMusicChaseSeeStop(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerChaseMusicSeeMaster[client]) + { + g_iPlayerChaseMusicSeeMaster[client] = -1; + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); + } + + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); +} + +public Action:Timer_PlayerFadeInMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; + + g_flPlayerMusicVolume[client] += 0.07; + if (g_flPlayerMusicVolume[client] > g_flPlayerMusicTargetVolume[client]) g_flPlayerMusicVolume[client] = g_flPlayerMusicTargetVolume[client]; + + if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); + + if (g_flPlayerMusicVolume[client] >= g_flPlayerMusicTargetVolume[client]) + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; + + g_flPlayerMusicVolume[client] -= 0.07; + if (g_flPlayerMusicVolume[client] < 0.0) g_flPlayerMusicVolume[client] = 0.0; + + if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); + + if (g_flPlayerMusicVolume[client] <= 0.0) + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeIn20DollarsMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayer20DollarsMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayer20DollarsMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] > 1.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayer20DollarsMusic[client][0]) EmitSoundToClient(client, g_strPlayer20DollarsMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); + + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOut20DollarsMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayer20DollarsMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayer20DollarsMusic[client], false)) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayer20DollarsMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] < 0.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); + + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInAlertMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerAlertMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerAlertMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerAlertMusic[client][0]) EmitSoundToClient(client, g_strPlayerAlertMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); + + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutAlertMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerAlertMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerAlertMusic[client], false)) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerAlertMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); + + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInChaseMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerChaseMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerChaseMusic[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInChaseMusicSee(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerChaseMusicSee[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusicSee[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutChaseMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerChaseMusic[client], false)) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerChaseMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutChaseMusicSee(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerChaseMusicSee[client], false)) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +stock bool:ClientHasMusicFlag(client, iFlag) +{ + return bool:(g_iPlayerMusicFlags[client] & iFlag); +} + +stock bool:ClientHasMusicFlag2(iValue, iFlag) +{ + return bool:(iValue & iFlag); +} + +stock ClientAddMusicFlag(client, iFlag) +{ + if (!ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] |= iFlag; +} + +stock ClientRemoveMusicFlag(client, iFlag) +{ + if (ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] &= ~iFlag; +} + +// ========================================================== +// MISC FUNCTIONS +// ========================================================== + +// This could be used for entities as well. +stock ClientStopAllSlenderSounds(client, const String:profileName[], const String:sectionName[], iChannel) +{ + if (!client || !IsValidEntity(client)) return; + + if (!IsProfileValid(profileName)) return; + + decl String:buffer[PLATFORM_MAX_PATH]; + + KvRewind(g_hConfig); + if (KvJumpToKey(g_hConfig, profileName)) + { + decl String:s[32]; + + if (KvJumpToKey(g_hConfig, sectionName)) + { + for (new i2 = 1;; i2++) + { + IntToString(i2, s, sizeof(s)); + KvGetString(g_hConfig, s, buffer, sizeof(buffer)); + if (!buffer[0]) break; + + StopSound(client, iChannel, buffer); + } + } + } +} + +stock ClientUpdateListeningFlags(client, bool:bReset=false) +{ + if (!IsClientInGame(client)) return; + + for (new i = 1; i <= MaxClients; i++) + { + if (i == client || !IsClientInGame(i)) continue; + + if (bReset || IsRoundEnding() || GetConVarBool(g_cvAllChat)) + { + SetListenOverride(client, i, Listen_Default); + continue; + } + + new MuteMode:iMuteMode = g_iPlayerPreferences[client][PlayerPreference_MuteMode]; + + if (g_bPlayerEliminated[client]) + { + if (!g_bPlayerEliminated[i]) + { + if (iMuteMode == MuteMode_DontHearOtherTeam) + { + SetListenOverride(client, i, Listen_No); + } + else if (iMuteMode == MuteMode_DontHearOtherTeamIfNotProxy && !g_bPlayerProxy[client]) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + if (!g_bPlayerEliminated[i]) + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (DidClientEscape(i)) + { + if (!DidClientEscape(client)) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + if (!DidClientEscape(client)) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + } + else + { + new bool:bCanHear = false; + if (GetConVarFloat(g_cvPlayerVoiceDistance) <= 0.0) bCanHear = true; + + if (!bCanHear) + { + decl Float:flMyPos[3], Float:flHisPos[3]; + GetClientEyePosition(client, flMyPos); + GetClientEyePosition(i, flHisPos); + + new Float:flDist = GetVectorDistance(flMyPos, flHisPos); + + if (GetConVarFloat(g_cvPlayerVoiceWallScale) > 0.0) + { + new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceRayDontHitCharacters); + new bool:bDidHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bDidHit) + { + flDist *= GetConVarFloat(g_cvPlayerVoiceWallScale); + } + } + + if (flDist <= GetConVarFloat(g_cvPlayerVoiceDistance)) + { + bCanHear = true; + } + } + + if (bCanHear) + { + if (IsClientInGhostMode(i) != IsClientInGhostMode(client) && + DidClientEscape(i) != DidClientEscape(client)) + { + bCanHear = false; + } + } + + if (bCanHear) + { + SetListenOverride(client, i, Listen_Default); + } + else + { + SetListenOverride(client, i, Listen_No); + } + } + } + else + { + SetListenOverride(client, i, Listen_No); + } + } + } +} + +stock ClientShowMainMessage(client, const String:sMessage[], any:...) +{ + decl String:message[512]; + VFormat(message, sizeof(message), sMessage, 3); + + SetHudTextParams(-1.0, 0.4, + 5.0, + 255, + 255, + 255, + 200, + 2, + 1.0, + 0.07, + 2.0); + ShowSyncHudText(client, g_hHudSync, message); +} + +stock ClientResetSlenderStats(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSlenderStats(%d)", client); +#endif + + g_flPlayerStress[client] = 0.0; + g_flPlayerStressNextUpdateTime[client] = -1.0; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_bPlayerSeesSlender[client][i] = false; + g_flPlayerSeesSlenderLastTime[client][i] = -1.0; + g_flPlayerSightSoundNextTime[client][i] = -1.0; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSlenderStats(%d)", client); +#endif +} + +bool:ClientSetQueuePoints(client, iAmount) +{ + if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return false; + g_iPlayerQueuePoints[client] = iAmount; + ClientSaveCookies(client); + return true; +} + +ClientSaveCookies(client) +{ + if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return; + + // Save and reset our queue points. + decl String:s[64]; + Format(s, sizeof(s), "%d ; %d ; %d ; %d ; %d ; %d", g_iPlayerQueuePoints[client], + g_iPlayerPreferences[client][PlayerPreference_ShowHints], + g_iPlayerPreferences[client][PlayerPreference_MuteMode], + g_iPlayerPreferences[client][PlayerPreference_FilmGrain], + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection], + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay]); + + SetClientCookie(client, g_hCookie, s); +} + +stock ClientViewPunch(client, const Float:angleOffset[3]) +{ + if (g_offsPlayerPunchAngleVel == -1) return; + + decl Float:flOffset[3]; + for (new i = 0; i < 3; i++) flOffset[i] = angleOffset[i]; + ScaleVector(flOffset, 20.0); + + /* + if (!IsFakeClient(client)) + { + // Latency compensation. + new Float:flLatency = GetClientLatency(client, NetFlow_Outgoing); + new Float:flLatencyCalcDiff = 60.0 * Pow(flLatency, 2.0); + + for (new i = 0; i < 3; i++) flOffset[i] += (flOffset[i] * flLatencyCalcDiff); + } + */ + + decl Float:flAngleVel[3]; + GetEntDataVector(client, g_offsPlayerPunchAngleVel, flAngleVel); + AddVectors(flAngleVel, flOffset, flOffset); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flOffset, true); +} + +public Action:Hook_ConstantGlowSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (EntRefToEntIndex(g_iPlayerConstantGlowEntity[i]) == ent) + { + iOwner = i; + break; + } + } + + if (iOwner != -1) + { + if (!IsPlayerAlive(iOwner) || g_bPlayerEliminated[iOwner]) return Plugin_Handled; + if (!IsPlayerAlive(other) || (!g_bPlayerProxy[other] && !IsClientInGhostMode(other))) return Plugin_Handled; + } + + return Plugin_Continue; +} + +stock ClientSetFOV(client, iFOV) +{ + SetEntData(client, g_offsPlayerFOV, iFOV); + SetEntData(client, g_offsPlayerDefaultFOV, iFOV); +} + +stock TF2_GetClassName(TFClassType:iClass, String:sBuffer[], sBufferLen) +{ + switch (iClass) + { + case TFClass_Scout: strcopy(sBuffer, sBufferLen, "scout"); + case TFClass_Sniper: strcopy(sBuffer, sBufferLen, "sniper"); + case TFClass_Soldier: strcopy(sBuffer, sBufferLen, "soldier"); + case TFClass_DemoMan: strcopy(sBuffer, sBufferLen, "demoman"); + case TFClass_Heavy: strcopy(sBuffer, sBufferLen, "heavyweapons"); + case TFClass_Medic: strcopy(sBuffer, sBufferLen, "medic"); + case TFClass_Pyro: strcopy(sBuffer, sBufferLen, "pyro"); + case TFClass_Spy: strcopy(sBuffer, sBufferLen, "spy"); + case TFClass_Engineer: strcopy(sBuffer, sBufferLen, "engineer"); + default: strcopy(sBuffer, sBufferLen, ""); + } +} + +#define EF_DIMLIGHT (1 << 2) + +stock ClientSDKFlashlightTurnOn(client) +{ + if (!IsValidClient(client)) return; + + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (iEffects & EF_DIMLIGHT) return; + + iEffects |= EF_DIMLIGHT; + + SetEntProp(client, Prop_Send, "m_fEffects", iEffects); +} + +stock ClientSDKFlashlightTurnOff(client) +{ + if (!IsValidClient(client)) return; + + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (!(iEffects & EF_DIMLIGHT)) return; + + iEffects &= ~EF_DIMLIGHT; + + SetEntProp(client, Prop_Send, "m_fEffects", iEffects); +} + +stock bool:IsPointVisibleToAPlayer(const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false) +{ + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (IsPointVisibleToPlayer(i, pos, bCheckFOV, bCheckBlink)) return true; + } + + return false; +} + +stock bool:IsPointVisibleToPlayer(client, const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || IsClientInGhostMode(client)) return false; + + if (bCheckEliminated && g_bPlayerEliminated[client]) return false; + + if (bCheckBlink && IsClientBlinking(client)) return false; + + decl Float:eyePos[3]; + GetClientEyePosition(client, eyePos); + + // Check fog, if we can. + if (g_offsPlayerFogCtrl != -1 && g_offsFogCtrlEnable != -1 && g_offsFogCtrlEnd != -1) + { + new iFogEntity = GetEntDataEnt2(client, g_offsPlayerFogCtrl); + if (IsValidEdict(iFogEntity)) + { + if (GetEntData(iFogEntity, g_offsFogCtrlEnable) && + GetVectorDistance(eyePos, pos) >= GetEntDataFloat(iFogEntity, g_offsFogCtrlEnd)) + { + return false; + } + } + } + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, pos, CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); + new bool:bHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bHit) return false; + + if (bCheckFOV) + { + decl Float:eyeAng[3], Float:reqVisibleAng[3]; + GetClientEyeAngles(client, eyeAng); + + new Float:flFOV = float(g_iPlayerDesiredFOV[client]); + SubtractVectors(pos, eyePos, reqVisibleAng); + GetVectorAngles(reqVisibleAng, reqVisibleAng); + + new Float:difference = FloatAbs(AngleDiff(eyeAng[0], reqVisibleAng[0])) + FloatAbs(AngleDiff(eyeAng[1], reqVisibleAng[1])); + if (difference > ((flFOV * 0.5) + 10.0)) return false; + } + + return true; +} + +public Action:Timer_ClientPostWeapons(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!IsPlayerAlive(client)) return; + + if (timer != g_hPlayerPostWeaponsTimer[client]) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) + { + DebugMessage("START Timer_ClientPostWeapons(%d)", client); + } + + new iOldWeaponItemIndexes[6] = { -1, ... }; + new iNewWeaponItemIndexes[6] = { -1, ... }; + + for (new i = 0; i <= 5; i++) + { + new iWeapon = GetPlayerWeaponSlot(client, i); + if (!IsValidEdict(iWeapon)) continue; + + iOldWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + } + +#endif + + new bool:bRemoveWeapons = true; + new bool:bRestrictWeapons = true; + + if (IsRoundEnding()) + { + if (!g_bPlayerEliminated[client]) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + } + + // pvp + if (IsClientInPvP(client)) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + + if (IsRoundInWarmup()) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + + if (IsClientInGhostMode(client)) + { + bRemoveWeapons = true; + } + + if (bRemoveWeapons) + { + for (new i = 0; i <= 5; i++) + { + if (i == TFWeaponSlot_Melee && !IsClientInGhostMode(client)) continue; + TF2_RemoveWeaponSlotAndWearables(client, i); + } + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_weapon_builder")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + + ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_wearable_demoshield")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + + ClientSwitchToWeaponSlot(client, TFWeaponSlot_Melee); + } + + if (bRestrictWeapons) + { + new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); + + if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) + { + new TFClassType:iPlayerClass = TF2_GetPlayerClass(client); + new Handle:hItem = INVALID_HANDLE; + + new iWeapon = INVALID_ENT_REFERENCE; + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + + if (IsValidEdict(iWeapon)) + { + if (IsWeaponRestricted(iPlayerClass, GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"))) + { + hItem = INVALID_HANDLE; + TF2_RemoveWeaponSlotAndWearables(client, iSlot); + + switch (iSlot) + { + case TFWeaponSlot_Primary: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponScattergun; + case TFClass_Sniper: hItem = g_hSDKWeaponSniperRifle; + case TFClass_Soldier: hItem = g_hSDKWeaponRocketLauncher; + case TFClass_DemoMan: hItem = g_hSDKWeaponGrenadeLauncher; + case TFClass_Heavy: hItem = g_hSDKWeaponMinigun; + case TFClass_Medic: hItem = g_hSDKWeaponSyringeGun; + case TFClass_Pyro: hItem = g_hSDKWeaponFlamethrower; + case TFClass_Spy: hItem = g_hSDKWeaponRevolver; + case TFClass_Engineer: hItem = g_hSDKWeaponShotgunPrimary; + } + } + case TFWeaponSlot_Secondary: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponPistolScout; + case TFClass_Sniper: hItem = g_hSDKWeaponSMG; + case TFClass_Soldier: hItem = g_hSDKWeaponShotgunSoldier; + case TFClass_DemoMan: hItem = g_hSDKWeaponStickyLauncher; + case TFClass_Heavy: hItem = g_hSDKWeaponShotgunHeavy; + case TFClass_Medic: hItem = g_hSDKWeaponMedigun; + case TFClass_Pyro: hItem = g_hSDKWeaponShotgunPyro; + case TFClass_Engineer: hItem = g_hSDKWeaponPistol; + } + } + case TFWeaponSlot_Melee: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponBat; + case TFClass_Sniper: hItem = g_hSDKWeaponKukri; + case TFClass_Soldier: hItem = g_hSDKWeaponShovel; + case TFClass_DemoMan: hItem = g_hSDKWeaponBottle; + case TFClass_Heavy: hItem = g_hSDKWeaponFists; + case TFClass_Medic: hItem = g_hSDKWeaponBonesaw; + case TFClass_Pyro: hItem = g_hSDKWeaponFireaxe; + case TFClass_Spy: hItem = g_hSDKWeaponKnife; + case TFClass_Engineer: hItem = g_hSDKWeaponWrench; + } + } + case 4: + { + switch (iPlayerClass) + { + case TFClass_Spy: hItem = g_hSDKWeaponInvis; + } + } + } + + if (hItem != INVALID_HANDLE) + { + new iNewWeapon = TF2Items_GiveNamedItem(client, hItem); + if (IsValidEntity(iNewWeapon)) + { + EquipPlayerWeapon(client, iNewWeapon); + } + } + } + } + } + } + + // Fixes the Pretty Boy's Pocket Pistol glitch. + new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, client); + if (iHealth > iMaxHealth) + { + SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); + SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); + } + } + + // Change stats on some weapons. + if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) + { + new iWeapon = INVALID_ENT_REFERENCE; + decl Handle:hWeapon; + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; + + new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + switch (iItemDef) + { + case 214: // Powerjack + { + TF2_RemoveWeaponSlot(client, iSlot); + + hWeapon = PrepareItemHandle("tf_weapon_fireaxe", 214, 0, 0, "180 ; 20.0 ; 206 ; 1.33"); + new iEnt = TF2Items_GiveNamedItem(client, hWeapon); + CloseHandle(hWeapon); + EquipPlayerWeapon(client, iEnt); + } + } + } + } + + // Remove all hats. + if (IsClientInGhostMode(client)) + { + new ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_wearable")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + } + +#if defined DEBUG + for (new i = 0; i <= 5; i++) + { + new iWeapon = GetPlayerWeaponSlot(client, i); + if (!IsValidEdict(iWeapon)) continue; + + iNewWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + } + + if (GetConVarInt(g_cvDebugDetail) > 0) + { + for (new i = 0; i <= 5; i++) + { + DebugMessage("-> slot %d: %d (old: %d)", i, iNewWeaponItemIndexes[i], iOldWeaponItemIndexes[i]); + } + + DebugMessage("END Timer_ClientPostWeapons(%d) -> remove = %d, restrict = %d", client, bRemoveWeapons, bRestrictWeapons); + } +#endif +} + +public Action:Timer_ApplyCustomModel(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + + if (g_bPlayerProxy[client] && iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + // Set custom model, if any. + decl String:sBuffer[PLATFORM_MAX_PATH]; + decl String:sSectionName[64]; + + decl String:sClassName[64]; + TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); + + Format(sSectionName, sizeof(sSectionName), "mod_proxy_%s", sClassName); + if ((GetRandomStringFromProfile(sProfile, sSectionName, sBuffer, sizeof(sBuffer)) && sBuffer[0]) || + (GetRandomStringFromProfile(sProfile, "mod_proxy_all", sBuffer, sizeof(sBuffer)) && sBuffer[0])) + { + SetVariantString(sBuffer); + AcceptEntityInput(client, "SetCustomModel"); + SetEntProp(client, Prop_Send, "m_bUseClassAnimations", true); + } + + if (IsPlayerAlive(client)) + { + // Play any sounds, if any. + if (GetRandomStringFromProfile(sProfile, "sound_proxy_spawn", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_spawn_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_spawn_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_spawn_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_spawn_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_spawn_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } +} + +bool:IsWeaponRestricted(TFClassType:iClass, iItemDef) +{ + if (g_hRestrictedWeaponsConfig == INVALID_HANDLE) return false; + + new bool:bReturn = false; + + decl String:sItemDef[32]; + IntToString(iItemDef, sItemDef, sizeof(sItemDef)); + + KvRewind(g_hRestrictedWeaponsConfig); + if (KvJumpToKey(g_hRestrictedWeaponsConfig, "all")) + { + bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef); + } + + new bool:bFoundSection = false; + KvRewind(g_hRestrictedWeaponsConfig); + + switch (iClass) + { + case TFClass_Scout: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "scout"); + case TFClass_Soldier: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "soldier"); + case TFClass_Sniper: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "sniper"); + case TFClass_DemoMan: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "demoman"); + case TFClass_Heavy: + { + bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavy"); + + if (!bFoundSection) + { + KvRewind(g_hRestrictedWeaponsConfig); + bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavyweapons"); + } + } + case TFClass_Medic: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "medic"); + case TFClass_Spy: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "spy"); + case TFClass_Pyro: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "pyro"); + case TFClass_Engineer: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "engineer"); + } + + if (bFoundSection) + { + bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef, bReturn); + } + + return bReturn; +} + +public Action:Timer_RespawnPlayer(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (IsPlayerAlive(client)) return; + + TF2_RespawnPlayer(client); } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/debug.sp b/addons/sourcemod/scripting/rytp_horror/debug.sp index 9e05614..8b48236 100644 --- a/addons/sourcemod/scripting/rytp_horror/debug.sp +++ b/addons/sourcemod/scripting/rytp_horror/debug.sp @@ -1,161 +1,161 @@ -#if defined _sf2_debug_included - #endinput -#endif -#define _sf2_debug_included - -#if !defined DEBUG - #endinput -#endif - -#define DEBUG_BOSS_TELEPORTATION (1 << 0) -#define DEBUG_BOSS_CHASE (1 << 1) -#define DEBUG_PLAYER_STRESS (1 << 2) -#define DEBUG_PLAYER_ACTION_SLOT (1 << 3) -#define DEBUG_BOSS_PROXIES (1 << 4) - -new g_iPlayerDebugFlags[MAXPLAYERS + 1] = { 0, ... }; - -static String:g_strDebugLogFilePath[512] = ""; - -new Handle:g_cvDebugDetail = INVALID_HANDLE; -new Handle:g_cvDebugBosses = INVALID_HANDLE; - -InitializeDebug() -{ - g_cvDebugDetail = CreateConVar("sf2_debug_detail", "0", "0 = off, 1 = debug only large, expensive functions, 2 = debug more events, 3 = debug client functions"); - g_cvDebugBosses = CreateConVar("sf2_debug_bosses", "0"); - - RegAdminCmd("sm_sf2_debug_boss_teleport", Command_DebugBossTeleport, ADMFLAG_CHEATS); - RegAdminCmd("sm_sf2_debug_boss_chase", Command_DebugBossChase, ADMFLAG_CHEATS); - RegAdminCmd("sm_sf2_debug_player_stress", Command_DebugPlayerStress, ADMFLAG_CHEATS); - RegAdminCmd("sm_sf2_debug_boss_proxies", Command_DebugBossProxies, ADMFLAG_CHEATS); -} - -InitializeDebugLogging() -{ - decl String:sDateSuffix[256]; - FormatTime(sDateSuffix, sizeof(sDateSuffix), "sf2-debug-%Y-%m-%d.log", GetTime()); - - BuildPath(Path_SM, g_strDebugLogFilePath, sizeof(g_strDebugLogFilePath), "logs/%s", sDateSuffix); - - decl String:sMap[64]; - GetCurrentMap(sMap, sizeof(sMap)); - - DebugMessage("-------- Mapchange to %s -------", sMap); -} - -stock DebugMessage(const String:sMessage[], any:...) -{ - decl String:sDebugMessage[1024], String:sTemp[1024]; - VFormat(sTemp, sizeof(sTemp), sMessage, 2); - Format(sDebugMessage, sizeof(sDebugMessage), "%s", sTemp); - //LogMessage(sDebugMessage); - LogToFile(g_strDebugLogFilePath, sDebugMessage); -} - -stock SendDebugMessageToPlayer(client, iDebugFlags, iType, const String:sMessage[], any:...) -{ - if (!IsClientInGame(client) || IsFakeClient(client)) return; - - decl String:sMsg[1024]; - VFormat(sMsg, sizeof(sMsg), sMessage, 5); - - if (g_iPlayerDebugFlags[client] & iDebugFlags) - { - switch (iType) - { - case 0: CPrintToChat(client, sMsg); - case 1: PrintCenterText(client, sMsg); - case 2: PrintHintText(client, sMsg); - } - } -} - -stock SendDebugMessageToPlayers(iDebugFlags, iType, const String:sMessage[], any:...) -{ - decl String:sMsg[1024]; - VFormat(sMsg, sizeof(sMsg), sMessage, 4); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i)) continue; - - if (g_iPlayerDebugFlags[i] & iDebugFlags) - { - switch (iType) - { - case 0: CPrintToChat(i, sMsg); - case 1: PrintCenterText(i, sMsg); - case 2: PrintHintText(i, sMsg); - } - } - } -} - -public Action:Command_DebugBossTeleport(client, args) -{ - new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_TELEPORTATION); - if (!bInMode) - { - g_iPlayerDebugFlags[client] |= DEBUG_BOSS_TELEPORTATION; - PrintToChat(client, "Enabled debugging boss teleportation."); - } - else - { - g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_TELEPORTATION; - PrintToChat(client, "Disabled debugging boss teleportation."); - } - - return Plugin_Handled; -} - -public Action:Command_DebugBossChase(client, args) -{ - new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_CHASE); - if (!bInMode) - { - g_iPlayerDebugFlags[client] |= DEBUG_BOSS_CHASE; - PrintToChat(client, "Enabled debugging boss chasing."); - } - else - { - g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_CHASE; - PrintToChat(client, "Disabled debugging boss chasing."); - } - - return Plugin_Handled; -} - -public Action:Command_DebugPlayerStress(client, args) -{ - new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_PLAYER_STRESS); - if (!bInMode) - { - g_iPlayerDebugFlags[client] |= DEBUG_PLAYER_STRESS; - PrintToChat(client, "Enabled debugging player stress."); - } - else - { - g_iPlayerDebugFlags[client] &= ~DEBUG_PLAYER_STRESS; - PrintToChat(client, "Disabled debugging player stress."); - } - - return Plugin_Handled; -} - -public Action:Command_DebugBossProxies(client, args) -{ - new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_PROXIES); - if (!bInMode) - { - g_iPlayerDebugFlags[client] |= DEBUG_BOSS_PROXIES; - PrintToChat(client, "Enabled debugging boss proxies."); - } - else - { - g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_PROXIES; - PrintToChat(client, "Disabled debugging boss proxies."); - } - - return Plugin_Handled; +#if defined _sf2_debug_included + #endinput +#endif +#define _sf2_debug_included + +#if !defined DEBUG + #endinput +#endif + +#define DEBUG_BOSS_TELEPORTATION (1 << 0) +#define DEBUG_BOSS_CHASE (1 << 1) +#define DEBUG_PLAYER_STRESS (1 << 2) +#define DEBUG_PLAYER_ACTION_SLOT (1 << 3) +#define DEBUG_BOSS_PROXIES (1 << 4) + +new g_iPlayerDebugFlags[MAXPLAYERS + 1] = { 0, ... }; + +static String:g_strDebugLogFilePath[512] = ""; + +new Handle:g_cvDebugDetail = INVALID_HANDLE; +new Handle:g_cvDebugBosses = INVALID_HANDLE; + +InitializeDebug() +{ + g_cvDebugDetail = CreateConVar("sf2_debug_detail", "0", "0 = off, 1 = debug only large, expensive functions, 2 = debug more events, 3 = debug client functions"); + g_cvDebugBosses = CreateConVar("sf2_debug_bosses", "0"); + + RegAdminCmd("sm_sf2_debug_boss_teleport", Command_DebugBossTeleport, ADMFLAG_CHEATS); + RegAdminCmd("sm_sf2_debug_boss_chase", Command_DebugBossChase, ADMFLAG_CHEATS); + RegAdminCmd("sm_sf2_debug_player_stress", Command_DebugPlayerStress, ADMFLAG_CHEATS); + RegAdminCmd("sm_sf2_debug_boss_proxies", Command_DebugBossProxies, ADMFLAG_CHEATS); +} + +InitializeDebugLogging() +{ + decl String:sDateSuffix[256]; + FormatTime(sDateSuffix, sizeof(sDateSuffix), "sf2-debug-%Y-%m-%d.log", GetTime()); + + BuildPath(Path_SM, g_strDebugLogFilePath, sizeof(g_strDebugLogFilePath), "logs/%s", sDateSuffix); + + decl String:sMap[64]; + GetCurrentMap(sMap, sizeof(sMap)); + + DebugMessage("-------- Mapchange to %s -------", sMap); +} + +stock DebugMessage(const String:sMessage[], any:...) +{ + decl String:sDebugMessage[1024], String:sTemp[1024]; + VFormat(sTemp, sizeof(sTemp), sMessage, 2); + Format(sDebugMessage, sizeof(sDebugMessage), "%s", sTemp); + //LogMessage(sDebugMessage); + LogToFile(g_strDebugLogFilePath, sDebugMessage); +} + +stock SendDebugMessageToPlayer(client, iDebugFlags, iType, const String:sMessage[], any:...) +{ + if (!IsClientInGame(client) || IsFakeClient(client)) return; + + decl String:sMsg[1024]; + VFormat(sMsg, sizeof(sMsg), sMessage, 5); + + if (g_iPlayerDebugFlags[client] & iDebugFlags) + { + switch (iType) + { + case 0: CPrintToChat(client, sMsg); + case 1: PrintCenterText(client, sMsg); + case 2: PrintHintText(client, sMsg); + } + } +} + +stock SendDebugMessageToPlayers(iDebugFlags, iType, const String:sMessage[], any:...) +{ + decl String:sMsg[1024]; + VFormat(sMsg, sizeof(sMsg), sMessage, 4); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) continue; + + if (g_iPlayerDebugFlags[i] & iDebugFlags) + { + switch (iType) + { + case 0: CPrintToChat(i, sMsg); + case 1: PrintCenterText(i, sMsg); + case 2: PrintHintText(i, sMsg); + } + } + } +} + +public Action:Command_DebugBossTeleport(client, args) +{ + new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_TELEPORTATION); + if (!bInMode) + { + g_iPlayerDebugFlags[client] |= DEBUG_BOSS_TELEPORTATION; + PrintToChat(client, "Enabled debugging boss teleportation."); + } + else + { + g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_TELEPORTATION; + PrintToChat(client, "Disabled debugging boss teleportation."); + } + + return Plugin_Handled; +} + +public Action:Command_DebugBossChase(client, args) +{ + new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_CHASE); + if (!bInMode) + { + g_iPlayerDebugFlags[client] |= DEBUG_BOSS_CHASE; + PrintToChat(client, "Enabled debugging boss chasing."); + } + else + { + g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_CHASE; + PrintToChat(client, "Disabled debugging boss chasing."); + } + + return Plugin_Handled; +} + +public Action:Command_DebugPlayerStress(client, args) +{ + new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_PLAYER_STRESS); + if (!bInMode) + { + g_iPlayerDebugFlags[client] |= DEBUG_PLAYER_STRESS; + PrintToChat(client, "Enabled debugging player stress."); + } + else + { + g_iPlayerDebugFlags[client] &= ~DEBUG_PLAYER_STRESS; + PrintToChat(client, "Disabled debugging player stress."); + } + + return Plugin_Handled; +} + +public Action:Command_DebugBossProxies(client, args) +{ + new bool:bInMode = bool:(g_iPlayerDebugFlags[client] & DEBUG_BOSS_PROXIES); + if (!bInMode) + { + g_iPlayerDebugFlags[client] |= DEBUG_BOSS_PROXIES; + PrintToChat(client, "Enabled debugging boss proxies."); + } + else + { + g_iPlayerDebugFlags[client] &= ~DEBUG_BOSS_PROXIES; + PrintToChat(client, "Disabled debugging boss proxies."); + } + + return Plugin_Handled; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/effects.sp b/addons/sourcemod/scripting/rytp_horror/effects.sp index 4a8ab94..2acc0c6 100644 --- a/addons/sourcemod/scripting/rytp_horror/effects.sp +++ b/addons/sourcemod/scripting/rytp_horror/effects.sp @@ -1,275 +1,275 @@ -#if defined _sf2_effects_included - #endinput -#endif -#define _sf2_effects_included - -enum EffectEvent -{ - EffectEvent_Invalid = -1, - EffectEvent_Constant = 0, - EffectEvent_HitPlayer, - EffectEvent_PlayerSeesBoss -}; - -enum EffectType -{ - EffectType_Invalid = -1, - EffectType_Steam = 0, - EffectType_DynamicLight -}; - -SlenderSpawnEffects(iBossIndex, EffectEvent:iEvent) -{ - if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) return; - - new iBossID = NPCGetUniqueID(iBossIndex); - if (iBossID == -1) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - if (!KvJumpToKey(g_hConfig, sProfile) || !KvJumpToKey(g_hConfig, "effects") || !KvGotoFirstSubKey(g_hConfig)) return; - - new Handle:hArray = CreateArray(64); - decl String:sSectionName[64]; - - do - { - KvGetSectionName(g_hConfig, sSectionName, sizeof(sSectionName)); - PushArrayString(hArray, sSectionName); - } - while (KvGotoNextKey(g_hConfig)); - - if (GetArraySize(hArray) == 0) - { - CloseHandle(hArray); - return; - } - - decl String:sEvent[64]; - GetEffectEventString(iEvent, sEvent, sizeof(sEvent)); - if (!sEvent[0]) - { - LogError("Could not spawn effects for boss %d: invalid event string!", iBossIndex); - CloseHandle(hArray); - return; - } - - new iSlender = NPCGetEntIndex(iBossIndex); - decl Float:flBasePos[3], Float:flBaseAng[3]; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - KvJumpToKey(g_hConfig, "effects"); - - for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) - { - GetArrayString(hArray, i, sSectionName, sizeof(sSectionName)); - KvJumpToKey(g_hConfig, sSectionName); - - // Validate effect event. Check to see if it matches with ours. - decl String:sEffectEvent[64]; - KvGetString(g_hConfig, "event", sEffectEvent, sizeof(sEffectEvent)); - if (StrEqual(sEffectEvent, sEvent, false)) - { - // Validate effect type. - decl String:sEffectType[64]; - KvGetString(g_hConfig, "type", sEffectType, sizeof(sEffectType)); - new EffectType:iEffectType = GetEffectTypeFromString(sEffectType); - - if (iEffectType != EffectType_Invalid) - { - // Check base position behavior. - decl String:sBasePosCustom[64]; - KvGetString(g_hConfig, "origin_custom", sBasePosCustom, sizeof(sBasePosCustom)); - if (StrEqual(sBasePosCustom, "&CURRENTTARGET&", false)) - { - new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); - if (!iTarget || iTarget == INVALID_ENT_REFERENCE) - { - LogError("Could not spawn effect %s for boss %d: unable to read position of target due to no target!"); - KvGoBack(g_hConfig); - continue; - } - - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBasePos); - } - else - { - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) - { - LogError("Could not spawn effect %s for boss %d: unable to read position due to boss entity not in game!"); - KvGoBack(g_hConfig); - continue; - } - - GetEntPropVector(iSlender, Prop_Data, "m_vecAbsOrigin", flBasePos); - } - - decl String:sBaseAngCustom[64]; - KvGetString(g_hConfig, "angles_custom", sBaseAngCustom, sizeof(sBaseAngCustom)); - if (StrEqual(sBaseAngCustom, "&CURRENTTARGET&", false)) - { - new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); - if (!iTarget || iTarget == INVALID_ENT_REFERENCE) - { - LogError("Could not spawn effect %s for boss %d: unable to read angles of target due to no target!"); - KvGoBack(g_hConfig); - continue; - } - - GetEntPropVector(iTarget, Prop_Data, "m_angAbsRotation", flBaseAng); - } - else - { - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) - { - LogError("Could not spawn effect %s for boss %d: unable to read angles due to boss entity not in game!"); - KvGoBack(g_hConfig); - continue; - } - - GetEntPropVector(iSlender, Prop_Data, "m_angAbsRotation", flBaseAng); - } - - new iEnt = -1; - - switch (iEffectType) - { - case EffectType_Steam: iEnt = CreateEntityByName("env_steam"); - case EffectType_DynamicLight: iEnt = CreateEntityByName("light_dynamic"); - } - - if (iEnt != -1) - { - decl String:sValue[PLATFORM_MAX_PATH]; - KvGetString(g_hConfig, "renderamt", sValue, sizeof(sValue), "255"); - DispatchKeyValue(iEnt, "renderamt", sValue); - KvGetString(g_hConfig, "rendermode", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "rendermode", sValue); - KvGetString(g_hConfig, "renderfx", sValue, sizeof(sValue), "0"); - DispatchKeyValue(iEnt, "renderfx", sValue); - KvGetString(g_hConfig, "spawnflags", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "spawnflags", sValue); - - switch (iEffectType) - { - case EffectType_Steam: - { - KvGetString(g_hConfig, "spreadspeed", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "SpreadSpeed", sValue); - KvGetString(g_hConfig, "speed", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "Speed", sValue); - KvGetString(g_hConfig, "startsize", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "StartSize", sValue); - KvGetString(g_hConfig, "endsize", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "EndSize", sValue); - KvGetString(g_hConfig, "rate", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "Rate", sValue); - KvGetString(g_hConfig, "jetlength", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "Jetlength", sValue); - KvGetString(g_hConfig, "rollspeed", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "RollSpeed", sValue); - KvGetString(g_hConfig, "particletype", sValue, sizeof(sValue)); - DispatchKeyValue(iEnt, "type", sValue); - DispatchSpawn(iEnt); - ActivateEntity(iEnt); - } - case EffectType_DynamicLight: - { - SetVariantInt(KvGetNum(g_hConfig, "brightness")); - AcceptEntityInput(iEnt, "Brightness"); - SetVariantFloat(KvGetFloat(g_hConfig, "distance")); - AcceptEntityInput(iEnt, "Distance"); - SetVariantFloat(KvGetFloat(g_hConfig, "distance")); - AcceptEntityInput(iEnt, "spotlight_radius"); - SetVariantInt(KvGetNum(g_hConfig, "cone")); - AcceptEntityInput(iEnt, "cone"); - DispatchSpawn(iEnt); - ActivateEntity(iEnt); - - new r, g, b, a; - KvGetColor(g_hConfig, "rendercolor", r, g, b, a); - SetEntityRenderColor(iEnt, r, g, b, a); - } - } - - decl Float:flEffectPos[3], Float:flEffectAng[3]; - - KvGetVector(g_hConfig, "origin", flEffectPos); - KvGetVector(g_hConfig, "angles", flEffectAng); - VectorTransform(flEffectPos, flBasePos, flBaseAng, flEffectPos); - AddVectors(flEffectAng, flBaseAng, flEffectAng); - TeleportEntity(iEnt, flEffectPos, flEffectAng, NULL_VECTOR); - - new Float:flLifeTime = KvGetFloat(g_hConfig, "lifetime"); - if (flLifeTime > 0.0) CreateTimer(flLifeTime, Timer_KillEntity, EntIndexToEntRef(iEnt), TIMER_FLAG_NO_MAPCHANGE); - - decl String:sParentCustom[64]; - KvGetString(g_hConfig, "parent_custom", sParentCustom, sizeof(sParentCustom)); - if (StrEqual(sParentCustom, "&CURRENTTARGET&", false)) - { - new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); - if (!iTarget || iTarget == INVALID_ENT_REFERENCE) - { - LogError("Could not parent effect %s of boss %d to current target: target does not exist!", sSectionName, iBossIndex); - KvGoBack(g_hConfig); - continue; - } - - SetVariantString("!activator"); - AcceptEntityInput(iEnt, "SetParent", iTarget); - } - else - { - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) - { - LogError("Could not parent effect %s of boss %d to itself: boss entity does not exist!", sSectionName, iBossIndex); - KvGoBack(g_hConfig); - continue; - } - - SetVariantString("!activator"); - AcceptEntityInput(iEnt, "SetParent", iSlender); - } - - switch (iEffectType) - { - case EffectType_Steam, - EffectType_DynamicLight: - { - AcceptEntityInput(iEnt, "TurnOn"); - } - } - } - } - else - { - LogError("Could not spawn effect %s for boss %d: invalid type!", sSectionName, iBossIndex); - } - } - - KvGoBack(g_hConfig); - } - - CloseHandle(hArray); -} - -stock GetEffectEventString(EffectEvent:iEvent, String:sBuffer[], iBufferLen) -{ - switch (iEvent) - { - case EffectEvent_Constant: strcopy(sBuffer, iBufferLen, "constant"); - case EffectEvent_HitPlayer: strcopy(sBuffer, iBufferLen, "boss_hitplayer"); - case EffectEvent_PlayerSeesBoss: strcopy(sBuffer, iBufferLen, "boss_seenbyplayer"); - default: strcopy(sBuffer, iBufferLen, ""); - } -} - -stock EffectType:GetEffectTypeFromString(const String:sType[]) -{ - if (StrEqual(sType, "steam", false)) return EffectType_Steam; - if (StrEqual(sType, "dynamiclight", false)) return EffectType_DynamicLight; - return EffectType_Invalid; -} +#if defined _sf2_effects_included + #endinput +#endif +#define _sf2_effects_included + +enum EffectEvent +{ + EffectEvent_Invalid = -1, + EffectEvent_Constant = 0, + EffectEvent_HitPlayer, + EffectEvent_PlayerSeesBoss +}; + +enum EffectType +{ + EffectType_Invalid = -1, + EffectType_Steam = 0, + EffectType_DynamicLight +}; + +SlenderSpawnEffects(iBossIndex, EffectEvent:iEvent) +{ + if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) return; + + new iBossID = NPCGetUniqueID(iBossIndex); + if (iBossID == -1) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + if (!KvJumpToKey(g_hConfig, sProfile) || !KvJumpToKey(g_hConfig, "effects") || !KvGotoFirstSubKey(g_hConfig)) return; + + new Handle:hArray = CreateArray(64); + decl String:sSectionName[64]; + + do + { + KvGetSectionName(g_hConfig, sSectionName, sizeof(sSectionName)); + PushArrayString(hArray, sSectionName); + } + while (KvGotoNextKey(g_hConfig)); + + if (GetArraySize(hArray) == 0) + { + CloseHandle(hArray); + return; + } + + decl String:sEvent[64]; + GetEffectEventString(iEvent, sEvent, sizeof(sEvent)); + if (!sEvent[0]) + { + LogError("Could not spawn effects for boss %d: invalid event string!", iBossIndex); + CloseHandle(hArray); + return; + } + + new iSlender = NPCGetEntIndex(iBossIndex); + decl Float:flBasePos[3], Float:flBaseAng[3]; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + KvJumpToKey(g_hConfig, "effects"); + + for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) + { + GetArrayString(hArray, i, sSectionName, sizeof(sSectionName)); + KvJumpToKey(g_hConfig, sSectionName); + + // Validate effect event. Check to see if it matches with ours. + decl String:sEffectEvent[64]; + KvGetString(g_hConfig, "event", sEffectEvent, sizeof(sEffectEvent)); + if (StrEqual(sEffectEvent, sEvent, false)) + { + // Validate effect type. + decl String:sEffectType[64]; + KvGetString(g_hConfig, "type", sEffectType, sizeof(sEffectType)); + new EffectType:iEffectType = GetEffectTypeFromString(sEffectType); + + if (iEffectType != EffectType_Invalid) + { + // Check base position behavior. + decl String:sBasePosCustom[64]; + KvGetString(g_hConfig, "origin_custom", sBasePosCustom, sizeof(sBasePosCustom)); + if (StrEqual(sBasePosCustom, "&CURRENTTARGET&", false)) + { + new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); + if (!iTarget || iTarget == INVALID_ENT_REFERENCE) + { + LogError("Could not spawn effect %s for boss %d: unable to read position of target due to no target!"); + KvGoBack(g_hConfig); + continue; + } + + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBasePos); + } + else + { + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) + { + LogError("Could not spawn effect %s for boss %d: unable to read position due to boss entity not in game!"); + KvGoBack(g_hConfig); + continue; + } + + GetEntPropVector(iSlender, Prop_Data, "m_vecAbsOrigin", flBasePos); + } + + decl String:sBaseAngCustom[64]; + KvGetString(g_hConfig, "angles_custom", sBaseAngCustom, sizeof(sBaseAngCustom)); + if (StrEqual(sBaseAngCustom, "&CURRENTTARGET&", false)) + { + new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); + if (!iTarget || iTarget == INVALID_ENT_REFERENCE) + { + LogError("Could not spawn effect %s for boss %d: unable to read angles of target due to no target!"); + KvGoBack(g_hConfig); + continue; + } + + GetEntPropVector(iTarget, Prop_Data, "m_angAbsRotation", flBaseAng); + } + else + { + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) + { + LogError("Could not spawn effect %s for boss %d: unable to read angles due to boss entity not in game!"); + KvGoBack(g_hConfig); + continue; + } + + GetEntPropVector(iSlender, Prop_Data, "m_angAbsRotation", flBaseAng); + } + + new iEnt = -1; + + switch (iEffectType) + { + case EffectType_Steam: iEnt = CreateEntityByName("env_steam"); + case EffectType_DynamicLight: iEnt = CreateEntityByName("light_dynamic"); + } + + if (iEnt != -1) + { + decl String:sValue[PLATFORM_MAX_PATH]; + KvGetString(g_hConfig, "renderamt", sValue, sizeof(sValue), "255"); + DispatchKeyValue(iEnt, "renderamt", sValue); + KvGetString(g_hConfig, "rendermode", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "rendermode", sValue); + KvGetString(g_hConfig, "renderfx", sValue, sizeof(sValue), "0"); + DispatchKeyValue(iEnt, "renderfx", sValue); + KvGetString(g_hConfig, "spawnflags", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "spawnflags", sValue); + + switch (iEffectType) + { + case EffectType_Steam: + { + KvGetString(g_hConfig, "spreadspeed", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "SpreadSpeed", sValue); + KvGetString(g_hConfig, "speed", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "Speed", sValue); + KvGetString(g_hConfig, "startsize", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "StartSize", sValue); + KvGetString(g_hConfig, "endsize", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "EndSize", sValue); + KvGetString(g_hConfig, "rate", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "Rate", sValue); + KvGetString(g_hConfig, "jetlength", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "Jetlength", sValue); + KvGetString(g_hConfig, "rollspeed", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "RollSpeed", sValue); + KvGetString(g_hConfig, "particletype", sValue, sizeof(sValue)); + DispatchKeyValue(iEnt, "type", sValue); + DispatchSpawn(iEnt); + ActivateEntity(iEnt); + } + case EffectType_DynamicLight: + { + SetVariantInt(KvGetNum(g_hConfig, "brightness")); + AcceptEntityInput(iEnt, "Brightness"); + SetVariantFloat(KvGetFloat(g_hConfig, "distance")); + AcceptEntityInput(iEnt, "Distance"); + SetVariantFloat(KvGetFloat(g_hConfig, "distance")); + AcceptEntityInput(iEnt, "spotlight_radius"); + SetVariantInt(KvGetNum(g_hConfig, "cone")); + AcceptEntityInput(iEnt, "cone"); + DispatchSpawn(iEnt); + ActivateEntity(iEnt); + + new r, g, b, a; + KvGetColor(g_hConfig, "rendercolor", r, g, b, a); + SetEntityRenderColor(iEnt, r, g, b, a); + } + } + + decl Float:flEffectPos[3], Float:flEffectAng[3]; + + KvGetVector(g_hConfig, "origin", flEffectPos); + KvGetVector(g_hConfig, "angles", flEffectAng); + VectorTransform(flEffectPos, flBasePos, flBaseAng, flEffectPos); + AddVectors(flEffectAng, flBaseAng, flEffectAng); + TeleportEntity(iEnt, flEffectPos, flEffectAng, NULL_VECTOR); + + new Float:flLifeTime = KvGetFloat(g_hConfig, "lifetime"); + if (flLifeTime > 0.0) CreateTimer(flLifeTime, Timer_KillEntity, EntIndexToEntRef(iEnt), TIMER_FLAG_NO_MAPCHANGE); + + decl String:sParentCustom[64]; + KvGetString(g_hConfig, "parent_custom", sParentCustom, sizeof(sParentCustom)); + if (StrEqual(sParentCustom, "&CURRENTTARGET&", false)) + { + new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); + if (!iTarget || iTarget == INVALID_ENT_REFERENCE) + { + LogError("Could not parent effect %s of boss %d to current target: target does not exist!", sSectionName, iBossIndex); + KvGoBack(g_hConfig); + continue; + } + + SetVariantString("!activator"); + AcceptEntityInput(iEnt, "SetParent", iTarget); + } + else + { + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) + { + LogError("Could not parent effect %s of boss %d to itself: boss entity does not exist!", sSectionName, iBossIndex); + KvGoBack(g_hConfig); + continue; + } + + SetVariantString("!activator"); + AcceptEntityInput(iEnt, "SetParent", iSlender); + } + + switch (iEffectType) + { + case EffectType_Steam, + EffectType_DynamicLight: + { + AcceptEntityInput(iEnt, "TurnOn"); + } + } + } + } + else + { + LogError("Could not spawn effect %s for boss %d: invalid type!", sSectionName, iBossIndex); + } + } + + KvGoBack(g_hConfig); + } + + CloseHandle(hArray); +} + +stock GetEffectEventString(EffectEvent:iEvent, String:sBuffer[], iBufferLen) +{ + switch (iEvent) + { + case EffectEvent_Constant: strcopy(sBuffer, iBufferLen, "constant"); + case EffectEvent_HitPlayer: strcopy(sBuffer, iBufferLen, "boss_hitplayer"); + case EffectEvent_PlayerSeesBoss: strcopy(sBuffer, iBufferLen, "boss_seenbyplayer"); + default: strcopy(sBuffer, iBufferLen, ""); + } +} + +stock EffectType:GetEffectTypeFromString(const String:sType[]) +{ + if (StrEqual(sType, "steam", false)) return EffectType_Steam; + if (StrEqual(sType, "dynamiclight", false)) return EffectType_DynamicLight; + return EffectType_Invalid; +} diff --git a/addons/sourcemod/scripting/rytp_horror/logging.sp b/addons/sourcemod/scripting/rytp_horror/logging.sp index 3ba4d29..af3223b 100644 --- a/addons/sourcemod/scripting/rytp_horror/logging.sp +++ b/addons/sourcemod/scripting/rytp_horror/logging.sp @@ -1,27 +1,27 @@ -#if defined _sf2_logging_included - #endinput -#endif -#define _sf2_logging_included - -static String:g_strLogFilePath[512] = ""; - -InitializeLogging() -{ - decl String:sDateSuffix[256]; - FormatTime(sDateSuffix, sizeof(sDateSuffix), "sf2-%Y-%m-%d.log", GetTime()); - - BuildPath(Path_SM, g_strLogFilePath, sizeof(g_strLogFilePath), "logs/%s", sDateSuffix); - - decl String:sMap[64]; - GetCurrentMap(sMap, sizeof(sMap)); - - LogSF2Message("-------- Mapchange to %s -------", sMap); -} - -stock LogSF2Message(const String:sMessage[], any:...) -{ - decl String:sLogMessage[1024], String:sTemp[1024]; - VFormat(sTemp, sizeof(sTemp), sMessage, 2); - Format(sLogMessage, sizeof(sLogMessage), "%s", sTemp); - LogToFile(g_strLogFilePath, sLogMessage); +#if defined _sf2_logging_included + #endinput +#endif +#define _sf2_logging_included + +static String:g_strLogFilePath[512] = ""; + +InitializeLogging() +{ + decl String:sDateSuffix[256]; + FormatTime(sDateSuffix, sizeof(sDateSuffix), "sf2-%Y-%m-%d.log", GetTime()); + + BuildPath(Path_SM, g_strLogFilePath, sizeof(g_strLogFilePath), "logs/%s", sDateSuffix); + + decl String:sMap[64]; + GetCurrentMap(sMap, sizeof(sMap)); + + LogSF2Message("-------- Mapchange to %s -------", sMap); +} + +stock LogSF2Message(const String:sMessage[], any:...) +{ + decl String:sLogMessage[1024], String:sTemp[1024]; + VFormat(sTemp, sizeof(sTemp), sMessage, 2); + Format(sLogMessage, sizeof(sLogMessage), "%s", sTemp); + LogToFile(g_strLogFilePath, sLogMessage); } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/menus.sp b/addons/sourcemod/scripting/rytp_horror/menus.sp index 94ecb5a..0416661 100644 --- a/addons/sourcemod/scripting/rytp_horror/menus.sp +++ b/addons/sourcemod/scripting/rytp_horror/menus.sp @@ -1,756 +1,840 @@ -#if defined _sf2_menus - #endinput -#endif - -#define _sf2_menus - -new Handle:g_hMenuMain; -new Handle:g_hMenuVoteDifficulty; -new Handle:g_hMenuGhostMode; -new Handle:g_hMenuHelp; -new Handle:g_hMenuHelpObjective; -new Handle:g_hMenuHelpObjective2; -new Handle:g_hMenuHelpCommands; -new Handle:g_hMenuHelpGhostMode; -new Handle:g_hMenuHelpSprinting; -new Handle:g_hMenuHelpControls; -new Handle:g_hMenuHelpClassInfo; -new Handle:g_hMenuSettings; -new Handle:g_hMenuCredits; -new Handle:g_hMenuCredits2; - -#include "rytp_horror/playergroups/menus.sp" -#include "rytp_horror/pvp/menus.sp" - -SetupMenus() -{ - decl String:buffer[512]; - - // Create menus. - g_hMenuMain = CreateMenu(Menu_Main); - SetMenuTitle(g_hMenuMain, "%t%t\n \n", "SF2 Prefix", "SF2 Main Menu Title"); - Format(buffer, sizeof(buffer), "%t (!slhelp)", "SF2 Help Menu Title"); - AddMenuItem(g_hMenuMain, "0", buffer); - Format(buffer, sizeof(buffer), "%t (!slnext)", "SF2 Queue Menu Title"); - AddMenuItem(g_hMenuMain, "0", buffer); - Format(buffer, sizeof(buffer), "%t (!slgroup)", "SF2 Group Main Menu Title"); - AddMenuItem(g_hMenuMain, "0", buffer); - //Format(buffer, sizeof(buffer), "%t (!slghost)", "SF2 Ghost Mode Menu Title"); - //AddMenuItem(g_hMenuMain, "0", buffer); - Format(buffer, sizeof(buffer), "%t (!slsettings)", "SF2 Settings Menu Title"); - AddMenuItem(g_hMenuMain, "0", buffer); - strcopy(buffer, sizeof(buffer), "Credits (!slcredits)"); - AddMenuItem(g_hMenuMain, "0", buffer); - - g_hMenuVoteDifficulty = CreateMenu(Menu_VoteDifficulty); - SetMenuTitle(g_hMenuVoteDifficulty, "%t%t\n \n", "SF2 Prefix", "SF2 Difficulty Vote Menu Title"); - Format(buffer, sizeof(buffer), "%t", "SF2 Normal Difficulty"); - AddMenuItem(g_hMenuVoteDifficulty, "1", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Hard Difficulty"); - AddMenuItem(g_hMenuVoteDifficulty, "2", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Insane Difficulty"); - AddMenuItem(g_hMenuVoteDifficulty, "3", buffer); - - g_hMenuGhostMode = CreateMenu(Menu_GhostMode); - SetMenuTitle(g_hMenuGhostMode, "%t%t\n \n", "SF2 Prefix", "SF2 Ghost Mode Menu Title"); - Format(buffer, sizeof(buffer), "Enable"); - AddMenuItem(g_hMenuGhostMode, "0", buffer); - Format(buffer, sizeof(buffer), "Disable"); - AddMenuItem(g_hMenuGhostMode, "1", buffer); - - g_hMenuHelp = CreateMenu(Menu_Help); - SetMenuTitle(g_hMenuHelp, "%t%t\n \n", "SF2 Prefix", "SF2 Help Menu Title"); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Objective Menu Title"); - AddMenuItem(g_hMenuHelp, "0", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Commands Menu Title"); - AddMenuItem(g_hMenuHelp, "1", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Class Info Menu Title"); - AddMenuItem(g_hMenuHelp, "2", buffer); - //Format(buffer, sizeof(buffer), "%t", "SF2 Help Ghost Mode Menu Title"); - //AddMenuItem(g_hMenuHelp, "3", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Sprinting And Stamina Menu Title"); - AddMenuItem(g_hMenuHelp, "3", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Controls Menu Title"); - AddMenuItem(g_hMenuHelp, "4", buffer); - SetMenuExitBackButton(g_hMenuHelp, true); - - g_hMenuHelpObjective = CreateMenu(Menu_HelpObjective); - SetMenuTitle(g_hMenuHelpObjective, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Objective Menu Title", "SF2 Help Objective Description"); - AddMenuItem(g_hMenuHelpObjective, "0", "Next"); - AddMenuItem(g_hMenuHelpObjective, "1", "Back"); - - g_hMenuHelpObjective2 = CreateMenu(Menu_HelpObjective2); - SetMenuTitle(g_hMenuHelpObjective2, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Objective Menu Title", "SF2 Help Objective Description 2"); - AddMenuItem(g_hMenuHelpObjective2, "0", "Back"); - - g_hMenuHelpCommands = CreateMenu(Menu_BackButtonOnly); - SetMenuTitle(g_hMenuHelpCommands, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Commands Menu Title", "SF2 Help Commands Description"); - AddMenuItem(g_hMenuHelpCommands, "0", "Back"); - - g_hMenuHelpGhostMode = CreateMenu(Menu_BackButtonOnly); - SetMenuTitle(g_hMenuHelpGhostMode, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Ghost Mode Menu Title", "SF2 Help Ghost Mode Description"); - AddMenuItem(g_hMenuHelpGhostMode, "0", "Back"); - - g_hMenuHelpSprinting = CreateMenu(Menu_BackButtonOnly); - SetMenuTitle(g_hMenuHelpSprinting, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Sprinting And Stamina Menu Title", "SF2 Help Sprinting And Stamina Description"); - AddMenuItem(g_hMenuHelpSprinting, "0", "Back"); - - g_hMenuHelpControls = CreateMenu(Menu_BackButtonOnly); - SetMenuTitle(g_hMenuHelpControls, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Controls Menu Title", "SF2 Help Controls Description"); - AddMenuItem(g_hMenuHelpControls, "0", "Back"); - - g_hMenuHelpClassInfo = CreateMenu(Menu_ClassInfo); - SetMenuTitle(g_hMenuHelpClassInfo, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Class Info Menu Title", "SF2 Help Class Info Description"); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Scout Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Scout", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Sniper Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Sniper", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Soldier Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Soldier", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Demoman Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Demoman", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Heavy Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Heavy", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Medic Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Medic", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Pyro Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Pyro", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Spy Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Spy", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Help Engineer Class Info Menu Title"); - AddMenuItem(g_hMenuHelpClassInfo, "Engineer", buffer); - SetMenuExitBackButton(g_hMenuHelpClassInfo, true); - - g_hMenuSettings = CreateMenu(Menu_Settings); - SetMenuTitle(g_hMenuSettings, "%t%t\n \n", "SF2 Prefix", "SF2 Settings Menu Title"); - //Format(buffer, sizeof(buffer), "%t", "SF2 Settings PvP Menu Title"); - //AddMenuItem(g_hMenuSettings, "0", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Settings Hints Menu Title"); - AddMenuItem(g_hMenuSettings, "0", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Settings Mute Mode Menu Title"); - AddMenuItem(g_hMenuSettings, "0", buffer); - Format(buffer, sizeof(buffer), "%t", "SF2 Settings Proxy Menu Title"); - AddMenuItem(g_hMenuSettings, "0", buffer); - SetMenuExitBackButton(g_hMenuSettings, true); - - g_hMenuCredits = CreateMenu(Menu_Credits); - - Format(buffer, sizeof(buffer), "%tCredits\n \n", "SF2 Prefix"); - StrCat(buffer, sizeof(buffer), "Coder: Kit o' Rifty\n"); - StrCat(buffer, sizeof(buffer), "Version: "); - StrCat(buffer, sizeof(buffer), PLUGIN_VERSION); - StrCat(buffer, sizeof(buffer), "\n \n"); - StrCat(buffer, sizeof(buffer), "Mark J. Hadley (AgentParsec) - The creator of the Slender game!\n"); - StrCat(buffer, sizeof(buffer), "Mark Steen - Composing the intro music"); - StrCat(buffer, sizeof(buffer), "Mammoth Mogul - for being a GREAT test subject\n"); - StrCat(buffer, sizeof(buffer), "Egosins - for offering to host this publicly\n"); - StrCat(buffer, sizeof(buffer), "Somberguy - suggestions and support\n"); - StrCat(buffer, sizeof(buffer), "Omi-Box - materials, maps, current Slender Man model, and more!\n"); - StrCat(buffer, sizeof(buffer), "Narry Gewman - imported first Slender Man model\n"); - StrCat(buffer, sizeof(buffer), "Simply Delicious - for the awesome camera overlay!\n"); - StrCat(buffer, sizeof(buffer), "Jason278 - Page models"); - StrCat(buffer, sizeof(buffer), "\n \n"); - - SetMenuTitle(g_hMenuCredits, buffer); - AddMenuItem(g_hMenuCredits, "0", "Next"); - AddMenuItem(g_hMenuCredits, "1", "Back"); - - g_hMenuCredits2 = CreateMenu(Menu_Credits2); - - Format(buffer, sizeof(buffer), "%tCredits\n \n", "SF2 Prefix"); - StrCat(buffer, sizeof(buffer), "And to all the peeps who alpha-tested this thing!\n \n"); - StrCat(buffer, sizeof(buffer), "Tofu\n"); - StrCat(buffer, sizeof(buffer), "Ace-Dashie\n"); - StrCat(buffer, sizeof(buffer), "Hobbes\n"); - StrCat(buffer, sizeof(buffer), "Diskein\n"); - StrCat(buffer, sizeof(buffer), "111112oo\n"); - StrCat(buffer, sizeof(buffer), "Incoheriant Chipmunk\n"); - StrCat(buffer, sizeof(buffer), "Shrow\n"); - StrCat(buffer, sizeof(buffer), "Liquid Vita\n"); - StrCat(buffer, sizeof(buffer), "Pinkle D Lies\n"); - StrCat(buffer, sizeof(buffer), "Ultimatefry\n \n"); - - SetMenuTitle(g_hMenuCredits2, buffer); - AddMenuItem(g_hMenuCredits2, "0", "Back"); - - PvP_SetupMenus(); -} - -public Menu_Main(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuHelp, param1, 30); - case 1: DisplayQueuePointsMenu(param1); - case 2: DisplayGroupMainMenuToClient(param1); - //case 3: DisplayMenu(g_hMenuGhostMode, param1, 30); - case 3: DisplayMenu(g_hMenuSettings, param1, 30); - case 4: DisplayMenu(g_hMenuCredits, param1, MENU_TIME_FOREVER); - } - } -} - -public Menu_VoteDifficulty(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_VoteEnd) - { - decl String:sInfo[64], String:sDisplay[256], String:sColor[32]; - GetMenuItem(menu, param1, sInfo, sizeof(sInfo), _, sDisplay, sizeof(sDisplay)); - - if (IsSpecialRoundRunning() && - (g_iSpecialRoundType == SPECIALROUND_INSANEDIFFICULTY/* || g_iSpecialRoundType == SPECIALROUND_DOUBLEMAXPLAYERS*/)) - { - SetConVarInt(g_cvDifficulty, Difficulty_Insane); - } - else - { - SetConVarString(g_cvDifficulty, sInfo); - } - - new iDifficulty = GetConVarInt(g_cvDifficulty); - switch (iDifficulty) - { - case Difficulty_Easy: - { - Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Easy Difficulty"); - strcopy(sColor, sizeof(sColor), "{green}"); - } - case Difficulty_Hard: - { - Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Hard Difficulty"); - strcopy(sColor, sizeof(sColor), "{orange}"); - } - case Difficulty_Insane: - { - Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Insane Difficulty"); - strcopy(sColor, sizeof(sColor), "{red}"); - } - default: - { - Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Normal Difficulty"); - strcopy(sColor, sizeof(sColor), "{yellow}"); - } - } - - CPrintToChatAll("%t %s%s", "SF2 Difficulty Vote Finished", sColor, sDisplay); - } -} - -public Menu_GhostMode(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - if (IsRoundEnding() || - IsRoundInWarmup() || - !g_bPlayerEliminated[param1] || - !IsClientParticipating(param1) || - g_bPlayerProxy[param1]) - { - CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Not Allowed", param1); - } - else - { - switch (param2) - { - case 0: - { - if (IsClientInGhostMode(param1)) CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Enabled Already", param1); - else - { - TF2_RespawnPlayer(param1); - ClientSetGhostModeState(param1, true); - HandlePlayerHUD(param1); - - CPrintToChat(param1, "{olive}%T", "SF2 Ghost Mode Enabled", param1); - } - } - case 1: - { - if (!IsClientInGhostMode(param1)) CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Disabled Already", param1); - else - { - ClientSetGhostModeState(param1, false); - TF2_RespawnPlayer(param1); - - CPrintToChat(param1, "{olive}%T", "SF2 Ghost Mode Disabled", param1); - } - } - } - } - } -} - -public Menu_Help(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuHelpObjective, param1, 30); - case 1: DisplayMenu(g_hMenuHelpCommands, param1, 30); - case 2: DisplayMenu(g_hMenuHelpClassInfo, param1, 30); - //case 3: DisplayMenu(g_hMenuHelpGhostMode, param1, 30); - case 3: DisplayMenu(g_hMenuHelpSprinting, param1, 30); - case 4: DisplayMenu(g_hMenuHelpControls, param1, 30); - } - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayMenu(g_hMenuMain, param1, 30); - } - } -} - -public Menu_HelpObjective(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuHelpObjective2, param1, 30); - case 1: DisplayMenu(g_hMenuHelp, param1, 30); - } - } -} - -public Menu_HelpObjective2(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuHelpObjective, param1, 30); - } - } -} - -public Menu_BackButtonOnly(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuHelp, param1, 30); - } - } -} - -public Menu_Credits(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuCredits2, param1, MENU_TIME_FOREVER); - case 1: DisplayMenu(g_hMenuMain, param1, 30); - } - } -} - -public Menu_ClassInfo(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayMenu(g_hMenuMain, param1, 30); - } - } - else if (action == MenuAction_Select) - { - decl String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - - new Handle:hMenu = CreateMenu(Menu_ClassInfoBackOnly); - - decl String:sTitle[64], String:sDescription[64]; - Format(sTitle, sizeof(sTitle), "SF2 Help %s Class Info Menu Title", sInfo); - Format(sDescription, sizeof(sDescription), "SF2 Help %s Class Info Description", sInfo); - - SetMenuTitle(hMenu, "%t%t\n \n%t\n \n", "SF2 Prefix", sTitle, sDescription); - AddMenuItem(hMenu, "0", "Back"); - DisplayMenu(hMenu, param1, 30); - } -} - -public Menu_ClassInfoBackOnly(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Select) - { - DisplayMenu(g_hMenuHelpClassInfo, param1, 30); - } -} - -public Menu_Settings(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - //case 0: DisplayMenu(g_hMenuSettingsPvP, param1, 30); - case 0: - { - decl String:sBuffer[512]; - Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Hints Menu Title", param1); - - new Handle:hPanel = CreatePanel(); - SetPanelTitle(hPanel, sBuffer); - - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); - DrawPanelItem(hPanel, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); - DrawPanelItem(hPanel, sBuffer); - - SendPanelToClient(hPanel, param1, Panel_SettingsHints, 30); - CloseHandle(hPanel); - } - case 1: - { - decl String:sBuffer[512]; - Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Mute Mode Menu Title", param1); - - new Handle:hPanel = CreatePanel(); - SetPanelTitle(hPanel, sBuffer); - - DrawPanelItem(hPanel, "Normal"); - DrawPanelItem(hPanel, "Mute opposing team"); - DrawPanelItem(hPanel, "Mute opposing team except when I'm a proxy"); - - SendPanelToClient(hPanel, param1, Panel_SettingsMuteMode, 30); - CloseHandle(hPanel); - } - case 2: - { - decl String:sBuffer[512]; - Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Proxy Menu Title", param1); - - new Handle:hPanel = CreatePanel(); - SetPanelTitle(hPanel, sBuffer); - - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); - DrawPanelItem(hPanel, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); - DrawPanelItem(hPanel, sBuffer); - - SendPanelToClient(hPanel, param1, Panel_SettingsProxy, 30); - CloseHandle(hPanel); - } - } - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayMenu(g_hMenuMain, param1, 30); - } - } -} - -public Panel_SettingsHints(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 1: - { - g_iPlayerPreferences[param1][PlayerPreference_ShowHints] = true; - ClientSaveCookies(param1); - CPrintToChat(param1, "%T", "SF2 Enabled Hints", param1); - } - case 2: - { - g_iPlayerPreferences[param1][PlayerPreference_ShowHints] = false; - ClientSaveCookies(param1); - CPrintToChat(param1, "%T", "SF2 Disabled Hints", param1); - } - } - - DisplayMenu(g_hMenuSettings, param1, 30); - } -} - -public Panel_SettingsProxy(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 1: - { - g_iPlayerPreferences[param1][PlayerPreference_EnableProxySelection] = true; - ClientSaveCookies(param1); - CPrintToChat(param1, "%T", "SF2 Enabled Proxy", param1); - } - case 2: - { - g_iPlayerPreferences[param1][PlayerPreference_EnableProxySelection] = false; - ClientSaveCookies(param1); - CPrintToChat(param1, "%T", "SF2 Disabled Proxy", param1); - } - } - - DisplayMenu(g_hMenuSettings, param1, 30); - } -} - -public Panel_SettingsMuteMode(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 1: - { - g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_Normal; - ClientUpdateListeningFlags(param1); - ClientSaveCookies(param1); - CPrintToChat(param1, "{lightgreen}Mute mode set to normal."); - } - case 2: - { - g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_DontHearOtherTeam; - ClientUpdateListeningFlags(param1); - ClientSaveCookies(param1); - CPrintToChat(param1, "{lightgreen}Muted opposing team."); - } - case 3: - { - g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_DontHearOtherTeamIfNotProxy; - ClientUpdateListeningFlags(param1); - ClientSaveCookies(param1); - CPrintToChat(param1, "{lightgreen}Muted opposing team, but settings will be automatically set to normal if you're a proxy."); - } - } - - DisplayMenu(g_hMenuSettings, param1, 30); - } -} - -public Menu_Credits2(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayMenu(g_hMenuCredits, param1, MENU_TIME_FOREVER); - } - } -} - -DisplayQueuePointsMenu(client) -{ - new Handle:menu = CreateMenu(Menu_QueuePoints); - new Handle:hQueueList = GetQueueList(); - - decl String:sBuffer[256]; - - if (GetArraySize(hQueueList)) - { - Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Reset Queue Points Option", client, g_iPlayerQueuePoints[client]); - AddMenuItem(menu, "ponyponypony", sBuffer); - - decl iIndex, String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - decl String:sInfo[256]; - - for (new i = 0, iSize = GetArraySize(hQueueList); i < iSize; i++) - { - if (!GetArrayCell(hQueueList, i, 2)) - { - iIndex = GetArrayCell(hQueueList, i); - - Format(sBuffer, sizeof(sBuffer), "%N - %d", iIndex, g_iPlayerQueuePoints[iIndex]); - Format(sInfo, sizeof(sInfo), "player_%d", GetClientUserId(iIndex)); - AddMenuItem(menu, sInfo, sBuffer, g_bPlayerPlaying[iIndex] ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); - } - else - { - iIndex = GetArrayCell(hQueueList, i); - if (GetPlayerGroupMemberCount(iIndex) > 1) - { - GetPlayerGroupName(iIndex, sGroupName, sizeof(sGroupName)); - - Format(sBuffer, sizeof(sBuffer), "[GROUP] %s - %d", sGroupName, GetPlayerGroupQueuePoints(iIndex)); - Format(sInfo, sizeof(sInfo), "group_%d", iIndex); - AddMenuItem(menu, sInfo, sBuffer, IsPlayerGroupPlaying(iIndex) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); - } - else - { - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient)) continue; - if (ClientGetPlayerGroup(iClient) == iIndex) - { - Format(sBuffer, sizeof(sBuffer), "%N - %d", iClient, g_iPlayerQueuePoints[iClient]); - Format(sInfo, sizeof(sInfo), "player_%d", GetClientUserId(iClient)); - AddMenuItem(menu, "player", sBuffer, g_bPlayerPlaying[iClient] ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); - break; - } - } - } - } - } - } - - CloseHandle(hQueueList); - - SetMenuTitle(menu, "%t%T\n \n", "SF2 Prefix", "SF2 Queue Menu Title", client); - SetMenuExitBackButton(menu, true); - DisplayMenu(menu, client, MENU_TIME_FOREVER); -} - -DisplayViewGroupMembersQueueMenu(client, iGroupIndex) -{ - if (!IsPlayerGroupActive(iGroupIndex)) - { - // The group isn't valid anymore. Take him back to the main menu. - DisplayQueuePointsMenu(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - new Handle:hPlayers = CreateArray(); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - new iTempGroup = ClientGetPlayerGroup(i); - if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; - - PushArrayCell(hPlayers, i); - } - - new iPlayerCount = GetArraySize(hPlayers); - if (iPlayerCount) - { - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - - new Handle:hMenu = CreateMenu(Menu_ViewGroupMembersQueue); - SetMenuTitle(hMenu, "%t%T (%s)\n \n", "SF2 Prefix", "SF2 View Group Members Menu Title", client, sGroupName); - - decl String:sUserId[32]; - decl String:sName[MAX_NAME_LENGTH * 2]; - - for (new i = 0; i < iPlayerCount; i++) - { - new iClient = GetArrayCell(hPlayers, i); - IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); - GetClientName(iClient, sName, sizeof(sName)); - if (GetPlayerGroupLeader(iGroupIndex) == iClient) StrCat(sName, sizeof(sName), " (LEADER)"); - - AddMenuItem(hMenu, sUserId, sName); - } - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - } - else - { - // No players! - DisplayQueuePointsMenu(client); - } - - CloseHandle(hPlayers); -} - -public Menu_ViewGroupMembersQueue(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_End: CloseHandle(menu); - case MenuAction_Select: DisplayQueuePointsMenu(param1); - case MenuAction_Cancel: - { - if (param2 == MenuCancel_ExitBack) DisplayQueuePointsMenu(param1); - } - } -} - -DisplayResetQueuePointsMenu(client) -{ - decl String:buffer[256]; - - new Handle:menu = CreateMenu(Menu_ResetQueuePoints); - Format(buffer, sizeof(buffer), "%T", "Yes", client); - AddMenuItem(menu, "0", buffer); - Format(buffer, sizeof(buffer), "%T", "No", client); - AddMenuItem(menu, "1", buffer); - SetMenuTitle(menu, "%T\n \n", "SF2 Should Reset Queue Points", client); - DisplayMenu(menu, client, MENU_TIME_FOREVER); -} - -public Menu_QueuePoints(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_Select: - { - new String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - - if (StrEqual(sInfo, "ponyponypony")) DisplayResetQueuePointsMenu(param1); - else if (!StrContains(sInfo, "player_")) - { - } - else if (!StrContains(sInfo, "group_")) - { - decl String:sIndex[64]; - strcopy(sIndex, sizeof(sIndex), sInfo); - ReplaceString(sIndex, sizeof(sIndex), "group_", ""); - DisplayViewGroupMembersQueueMenu(param1, StringToInt(sIndex)); - } - } - case MenuAction_Cancel: - { - if (param2 == MenuCancel_ExitBack) - { - DisplayMenu(g_hMenuMain, param1, 30); - } - } - case MenuAction_End: CloseHandle(menu); - } -} - -public Menu_ResetQueuePoints(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_Select: - { - switch (param2) - { - case 0: - { - ClientSetQueuePoints(param1, 0); - CPrintToChat(param1, "{olive}%T", "SF2 Queue Points Reset", param1); - - // Special round. - if (IsSpecialRoundRunning()) - { - SetClientPlaySpecialRoundState(param1, true); - } - - // new boss round - if (IsNewBossRoundRunning()) - { - // If the player resets the queue points ignore them when checking for players that haven't played the new boss yet, if applicable. - SetClientPlayNewBossRoundState(param1, true); - } - } - } - - DisplayQueuePointsMenu(param1); - } - - case MenuAction_End: CloseHandle(menu); - } +#if defined _sf2_menus + #endinput +#endif + +#define _sf2_menus + +new Handle:g_hMenuMain; +new Handle:g_hMenuVoteDifficulty; +new Handle:g_hMenuGhostMode; +new Handle:g_hMenuHelp; +new Handle:g_hMenuHelpObjective; +new Handle:g_hMenuHelpObjective2; +new Handle:g_hMenuHelpCommands; +new Handle:g_hMenuHelpGhostMode; +new Handle:g_hMenuHelpSprinting; +new Handle:g_hMenuHelpControls; +new Handle:g_hMenuHelpClassInfo; +new Handle:g_hMenuSettings; +new Handle:g_hMenuCredits; +new Handle:g_hMenuCredits2; + +#include "rytp_horror/playergroups/menus.sp" +#include "rytp_horror/pvp/menus.sp" + +SetupMenus() +{ + decl String:buffer[512]; + + // Create menus. + g_hMenuMain = CreateMenu(Menu_Main); + SetMenuTitle(g_hMenuMain, "%t%t\n \n", "SF2 Prefix", "SF2 Main Menu Title"); + Format(buffer, sizeof(buffer), "%t (!slhelp)", "SF2 Help Menu Title"); + AddMenuItem(g_hMenuMain, "0", buffer); + Format(buffer, sizeof(buffer), "%t (!slnext)", "SF2 Queue Menu Title"); + AddMenuItem(g_hMenuMain, "0", buffer); + Format(buffer, sizeof(buffer), "%t (!slgroup)", "SF2 Group Main Menu Title"); + AddMenuItem(g_hMenuMain, "0", buffer); + Format(buffer, sizeof(buffer), "%t (!slghost)", "SF2 Ghost Mode Menu Title"); + AddMenuItem(g_hMenuMain, "0", buffer); + Format(buffer, sizeof(buffer), "%t (!slsettings)", "SF2 Settings Menu Title"); + AddMenuItem(g_hMenuMain, "0", buffer); + strcopy(buffer, sizeof(buffer), "Credits (!slcredits)"); + AddMenuItem(g_hMenuMain, "0", buffer); + + g_hMenuVoteDifficulty = CreateMenu(Menu_VoteDifficulty); + SetMenuTitle(g_hMenuVoteDifficulty, "%t%t\n \n", "SF2 Prefix", "SF2 Difficulty Vote Menu Title"); + Format(buffer, sizeof(buffer), "%t", "SF2 Normal Difficulty"); + AddMenuItem(g_hMenuVoteDifficulty, "1", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Hard Difficulty"); + AddMenuItem(g_hMenuVoteDifficulty, "2", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Insane Difficulty"); + AddMenuItem(g_hMenuVoteDifficulty, "3", buffer); + + g_hMenuGhostMode = CreateMenu(Menu_GhostMode); + SetMenuTitle(g_hMenuGhostMode, "%t%t\n \n", "SF2 Prefix", "SF2 Ghost Mode Menu Title"); + Format(buffer, sizeof(buffer), "Enable"); + AddMenuItem(g_hMenuGhostMode, "0", buffer); + Format(buffer, sizeof(buffer), "Disable"); + AddMenuItem(g_hMenuGhostMode, "1", buffer); + + g_hMenuHelp = CreateMenu(Menu_Help); + SetMenuTitle(g_hMenuHelp, "%t%t\n \n", "SF2 Prefix", "SF2 Help Menu Title"); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Objective Menu Title"); + AddMenuItem(g_hMenuHelp, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Commands Menu Title"); + AddMenuItem(g_hMenuHelp, "1", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Class Info Menu Title"); + AddMenuItem(g_hMenuHelp, "2", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Ghost Mode Menu Title"); + AddMenuItem(g_hMenuHelp, "3", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Sprinting And Stamina Menu Title"); + AddMenuItem(g_hMenuHelp, "4", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Controls Menu Title"); + AddMenuItem(g_hMenuHelp, "5", buffer); + SetMenuExitBackButton(g_hMenuHelp, true); + + g_hMenuHelpObjective = CreateMenu(Menu_HelpObjective); + SetMenuTitle(g_hMenuHelpObjective, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Objective Menu Title", "SF2 Help Objective Description"); + AddMenuItem(g_hMenuHelpObjective, "0", "Next"); + AddMenuItem(g_hMenuHelpObjective, "1", "Back"); + + g_hMenuHelpObjective2 = CreateMenu(Menu_HelpObjective2); + SetMenuTitle(g_hMenuHelpObjective2, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Objective Menu Title", "SF2 Help Objective Description 2"); + AddMenuItem(g_hMenuHelpObjective2, "0", "Back"); + + g_hMenuHelpCommands = CreateMenu(Menu_BackButtonOnly); + SetMenuTitle(g_hMenuHelpCommands, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Commands Menu Title", "SF2 Help Commands Description"); + AddMenuItem(g_hMenuHelpCommands, "0", "Back"); + + g_hMenuHelpGhostMode = CreateMenu(Menu_BackButtonOnly); + SetMenuTitle(g_hMenuHelpGhostMode, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Ghost Mode Menu Title", "SF2 Help Ghost Mode Description"); + AddMenuItem(g_hMenuHelpGhostMode, "0", "Back"); + + g_hMenuHelpSprinting = CreateMenu(Menu_BackButtonOnly); + SetMenuTitle(g_hMenuHelpSprinting, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Sprinting And Stamina Menu Title", "SF2 Help Sprinting And Stamina Description"); + AddMenuItem(g_hMenuHelpSprinting, "0", "Back"); + + g_hMenuHelpControls = CreateMenu(Menu_BackButtonOnly); + SetMenuTitle(g_hMenuHelpControls, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Controls Menu Title", "SF2 Help Controls Description"); + AddMenuItem(g_hMenuHelpControls, "0", "Back"); + + g_hMenuHelpClassInfo = CreateMenu(Menu_ClassInfo); + SetMenuTitle(g_hMenuHelpClassInfo, "%t%t\n \n%t\n \n", "SF2 Prefix", "SF2 Help Class Info Menu Title", "SF2 Help Class Info Description"); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Scout Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Scout", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Sniper Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Sniper", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Soldier Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Soldier", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Demoman Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Demoman", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Heavy Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Heavy", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Medic Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Medic", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Pyro Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Pyro", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Spy Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Spy", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Help Engineer Class Info Menu Title"); + AddMenuItem(g_hMenuHelpClassInfo, "Engineer", buffer); + SetMenuExitBackButton(g_hMenuHelpClassInfo, true); + + g_hMenuSettings = CreateMenu(Menu_Settings); + SetMenuTitle(g_hMenuSettings, "%t%t\n \n", "SF2 Prefix", "SF2 Settings Menu Title"); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings PvP Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings Hints Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings Mute Mode Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings Film Grain Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings Proxy Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + Format(buffer, sizeof(buffer), "%t", "SF2 Settings Ghost Overlay Menu Title"); + AddMenuItem(g_hMenuSettings, "0", buffer); + SetMenuExitBackButton(g_hMenuSettings, true); + + g_hMenuCredits = CreateMenu(Menu_Credits); + + Format(buffer, sizeof(buffer), "%tCredits\n \n", "SF2 Prefix"); + StrCat(buffer, sizeof(buffer), "Coder: Kit o' Rifty\n"); + StrCat(buffer, sizeof(buffer), "Version: "); + StrCat(buffer, sizeof(buffer), PLUGIN_VERSION); + StrCat(buffer, sizeof(buffer), "\n \n"); + StrCat(buffer, sizeof(buffer), "Mark J. Hadley (AgentParsec) - The creator of the Slender game!\n"); + StrCat(buffer, sizeof(buffer), "Mark Steen - Composing the intro music"); + StrCat(buffer, sizeof(buffer), "Mammoth Mogul - for being a GREAT test subject\n"); + StrCat(buffer, sizeof(buffer), "Egosins - for offering to host this publicly\n"); + StrCat(buffer, sizeof(buffer), "Somberguy - suggestions and support\n"); + StrCat(buffer, sizeof(buffer), "Omi-Box - materials, maps, current Slender Man model, and more!\n"); + StrCat(buffer, sizeof(buffer), "Narry Gewman - imported first Slender Man model\n"); + StrCat(buffer, sizeof(buffer), "Simply Delicious - for the awesome camera overlay!\n"); + StrCat(buffer, sizeof(buffer), "Jason278 - Page models"); + StrCat(buffer, sizeof(buffer), "\n \n"); + + SetMenuTitle(g_hMenuCredits, buffer); + AddMenuItem(g_hMenuCredits, "0", "Next"); + AddMenuItem(g_hMenuCredits, "1", "Back"); + + g_hMenuCredits2 = CreateMenu(Menu_Credits2); + + Format(buffer, sizeof(buffer), "%tCredits\n \n", "SF2 Prefix"); + StrCat(buffer, sizeof(buffer), "And to all the peeps who alpha-tested this thing!\n \n"); + StrCat(buffer, sizeof(buffer), "Tofu\n"); + StrCat(buffer, sizeof(buffer), "Ace-Dashie\n"); + StrCat(buffer, sizeof(buffer), "Hobbes\n"); + StrCat(buffer, sizeof(buffer), "Diskein\n"); + StrCat(buffer, sizeof(buffer), "111112oo\n"); + StrCat(buffer, sizeof(buffer), "Incoheriant Chipmunk\n"); + StrCat(buffer, sizeof(buffer), "Shrow\n"); + StrCat(buffer, sizeof(buffer), "Liquid Vita\n"); + StrCat(buffer, sizeof(buffer), "Pinkle D Lies\n"); + StrCat(buffer, sizeof(buffer), "Ultimatefry\n \n"); + + SetMenuTitle(g_hMenuCredits2, buffer); + AddMenuItem(g_hMenuCredits2, "0", "Back"); + + PvP_SetupMenus(); +} + +public Menu_Main(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuHelp, param1, 30); + case 1: DisplayQueuePointsMenu(param1); + case 2: DisplayGroupMainMenuToClient(param1); + case 3: DisplayMenu(g_hMenuGhostMode, param1, 30); + case 4: DisplayMenu(g_hMenuSettings, param1, 30); + case 5: DisplayMenu(g_hMenuCredits, param1, MENU_TIME_FOREVER); + } + } +} + +public Menu_VoteDifficulty(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_VoteEnd) + { + decl String:sInfo[64], String:sDisplay[256], String:sColor[32]; + GetMenuItem(menu, param1, sInfo, sizeof(sInfo), _, sDisplay, sizeof(sDisplay)); + + if (IsSpecialRoundRunning() && + (g_iSpecialRoundType == SPECIALROUND_INSANEDIFFICULTY || g_iSpecialRoundType == SPECIALROUND_DOUBLEMAXPLAYERS)) + { + SetConVarInt(g_cvDifficulty, Difficulty_Insane); + } + else + { + SetConVarString(g_cvDifficulty, sInfo); + } + + new iDifficulty = GetConVarInt(g_cvDifficulty); + switch (iDifficulty) + { + case Difficulty_Easy: + { + Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Easy Difficulty"); + strcopy(sColor, sizeof(sColor), "{green}"); + } + case Difficulty_Hard: + { + Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Hard Difficulty"); + strcopy(sColor, sizeof(sColor), "{orange}"); + } + case Difficulty_Insane: + { + Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Insane Difficulty"); + strcopy(sColor, sizeof(sColor), "{red}"); + } + default: + { + Format(sDisplay, sizeof(sDisplay), "%t", "SF2 Normal Difficulty"); + strcopy(sColor, sizeof(sColor), "{yellow}"); + } + } + + CPrintToChatAll("%t %s%s", "SF2 Difficulty Vote Finished", sColor, sDisplay); + } +} + +public Menu_GhostMode(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + if (IsRoundEnding() || + IsRoundInWarmup() || + !g_bPlayerEliminated[param1] || + !IsClientParticipating(param1) || + g_bPlayerProxy[param1]) + { + CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Not Allowed", param1); + } + else + { + switch (param2) + { + case 0: + { + if (IsClientInGhostMode(param1)) CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Enabled Already", param1); + else + { + TF2_RespawnPlayer(param1); + ClientSetGhostModeState(param1, true); + HandlePlayerHUD(param1); + + CPrintToChat(param1, "{olive}%T", "SF2 Ghost Mode Enabled", param1); + } + } + case 1: + { + if (!IsClientInGhostMode(param1)) CPrintToChat(param1, "{red}%T", "SF2 Ghost Mode Disabled Already", param1); + else + { + ClientSetGhostModeState(param1, false); + TF2_RespawnPlayer(param1); + + CPrintToChat(param1, "{olive}%T", "SF2 Ghost Mode Disabled", param1); + } + } + } + } + } +} + +public Menu_Help(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuHelpObjective, param1, 30); + case 1: DisplayMenu(g_hMenuHelpCommands, param1, 30); + case 2: DisplayMenu(g_hMenuHelpClassInfo, param1, 30); + case 3: DisplayMenu(g_hMenuHelpGhostMode, param1, 30); + case 4: DisplayMenu(g_hMenuHelpSprinting, param1, 30); + case 5: DisplayMenu(g_hMenuHelpControls, param1, 30); + } + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayMenu(g_hMenuMain, param1, 30); + } + } +} + +public Menu_HelpObjective(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuHelpObjective2, param1, 30); + case 1: DisplayMenu(g_hMenuHelp, param1, 30); + } + } +} + +public Menu_HelpObjective2(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuHelpObjective, param1, 30); + } + } +} + +public Menu_BackButtonOnly(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuHelp, param1, 30); + } + } +} + +public Menu_Credits(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuCredits2, param1, MENU_TIME_FOREVER); + case 1: DisplayMenu(g_hMenuMain, param1, 30); + } + } +} + +public Menu_ClassInfo(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayMenu(g_hMenuMain, param1, 30); + } + } + else if (action == MenuAction_Select) + { + decl String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + + new Handle:hMenu = CreateMenu(Menu_ClassInfoBackOnly); + + decl String:sTitle[64], String:sDescription[64]; + Format(sTitle, sizeof(sTitle), "SF2 Help %s Class Info Menu Title", sInfo); + Format(sDescription, sizeof(sDescription), "SF2 Help %s Class Info Description", sInfo); + + SetMenuTitle(hMenu, "%t%t\n \n%t\n \n", "SF2 Prefix", sTitle, sDescription); + AddMenuItem(hMenu, "0", "Back"); + DisplayMenu(hMenu, param1, 30); + } +} + +public Menu_ClassInfoBackOnly(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Select) + { + DisplayMenu(g_hMenuHelpClassInfo, param1, 30); + } +} + +public Menu_Settings(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuSettingsPvP, param1, 30); + case 1: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Hints Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + DrawPanelItem(hPanel, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + DrawPanelItem(hPanel, sBuffer); + + SendPanelToClient(hPanel, param1, Panel_SettingsHints, 30); + CloseHandle(hPanel); + } + case 2: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Mute Mode Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + DrawPanelItem(hPanel, "Normal"); + DrawPanelItem(hPanel, "Mute opposing team"); + DrawPanelItem(hPanel, "Mute opposing team except when I'm a proxy"); + + SendPanelToClient(hPanel, param1, Panel_SettingsMuteMode, 30); + CloseHandle(hPanel); + } + case 3: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Film Grain Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + DrawPanelItem(hPanel, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + DrawPanelItem(hPanel, sBuffer); + + SendPanelToClient(hPanel, param1, Panel_SettingsFilmGrain, 30); + CloseHandle(hPanel); + } + case 4: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Proxy Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + DrawPanelItem(hPanel, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + DrawPanelItem(hPanel, sBuffer); + + SendPanelToClient(hPanel, param1, Panel_SettingsProxy, 30); + CloseHandle(hPanel); + } + case 5: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings Ghost Overlay Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + DrawPanelItem(hPanel, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + DrawPanelItem(hPanel, sBuffer); + + SendPanelToClient(hPanel, param1, Panel_SettingsGhostOverlay, 30); + CloseHandle(hPanel); + } + } + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayMenu(g_hMenuMain, param1, 30); + } + } +} + +public Panel_SettingsFilmGrain(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_FilmGrain] = true; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Enabled Film Grain", param1); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_FilmGrain] = false; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Disabled Film Grain", param1); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } +} + +public Panel_SettingsHints(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_ShowHints] = true; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Enabled Hints", param1); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_ShowHints] = false; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Disabled Hints", param1); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } +} + +public Panel_SettingsProxy(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_EnableProxySelection] = true; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Enabled Proxy", param1); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_EnableProxySelection] = false; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Disabled Proxy", param1); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } +} + +public Panel_SettingsGhostOverlay(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_GhostOverlay] = true; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Enabled Ghost Overlay", param1); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_GhostOverlay] = false; + ClientSaveCookies(param1); + CPrintToChat(param1, "%T", "SF2 Disabled Ghost Overlay", param1); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } +} + +public Panel_SettingsMuteMode(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_Normal; + ClientUpdateListeningFlags(param1); + ClientSaveCookies(param1); + CPrintToChat(param1, "{lightgreen}Mute mode set to normal."); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_DontHearOtherTeam; + ClientUpdateListeningFlags(param1); + ClientSaveCookies(param1); + CPrintToChat(param1, "{lightgreen}Muted opposing team."); + } + case 3: + { + g_iPlayerPreferences[param1][PlayerPreference_MuteMode] = MuteMode_DontHearOtherTeamIfNotProxy; + ClientUpdateListeningFlags(param1); + ClientSaveCookies(param1); + CPrintToChat(param1, "{lightgreen}Muted opposing team, but settings will be automatically set to normal if you're a proxy."); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } +} + +public Menu_Credits2(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayMenu(g_hMenuCredits, param1, MENU_TIME_FOREVER); + } + } +} + +DisplayQueuePointsMenu(client) +{ + new Handle:menu = CreateMenu(Menu_QueuePoints); + new Handle:hQueueList = GetQueueList(); + + decl String:sBuffer[256]; + + if (GetArraySize(hQueueList)) + { + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Reset Queue Points Option", client, g_iPlayerQueuePoints[client]); + AddMenuItem(menu, "ponyponypony", sBuffer); + + decl iIndex, String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + decl String:sInfo[256]; + + for (new i = 0, iSize = GetArraySize(hQueueList); i < iSize; i++) + { + if (!GetArrayCell(hQueueList, i, 2)) + { + iIndex = GetArrayCell(hQueueList, i); + + Format(sBuffer, sizeof(sBuffer), "%N - %d", iIndex, g_iPlayerQueuePoints[iIndex]); + Format(sInfo, sizeof(sInfo), "player_%d", GetClientUserId(iIndex)); + AddMenuItem(menu, sInfo, sBuffer, g_bPlayerPlaying[iIndex] ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + } + else + { + iIndex = GetArrayCell(hQueueList, i); + if (GetPlayerGroupMemberCount(iIndex) > 1) + { + GetPlayerGroupName(iIndex, sGroupName, sizeof(sGroupName)); + + Format(sBuffer, sizeof(sBuffer), "[GROUP] %s - %d", sGroupName, GetPlayerGroupQueuePoints(iIndex)); + Format(sInfo, sizeof(sInfo), "group_%d", iIndex); + AddMenuItem(menu, sInfo, sBuffer, IsPlayerGroupPlaying(iIndex) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + } + else + { + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient)) continue; + if (ClientGetPlayerGroup(iClient) == iIndex) + { + Format(sBuffer, sizeof(sBuffer), "%N - %d", iClient, g_iPlayerQueuePoints[iClient]); + Format(sInfo, sizeof(sInfo), "player_%d", GetClientUserId(iClient)); + AddMenuItem(menu, "player", sBuffer, g_bPlayerPlaying[iClient] ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + break; + } + } + } + } + } + } + + CloseHandle(hQueueList); + + SetMenuTitle(menu, "%t%T\n \n", "SF2 Prefix", "SF2 Queue Menu Title", client); + SetMenuExitBackButton(menu, true); + DisplayMenu(menu, client, MENU_TIME_FOREVER); +} + +DisplayViewGroupMembersQueueMenu(client, iGroupIndex) +{ + if (!IsPlayerGroupActive(iGroupIndex)) + { + // The group isn't valid anymore. Take him back to the main menu. + DisplayQueuePointsMenu(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + new Handle:hPlayers = CreateArray(); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + new iTempGroup = ClientGetPlayerGroup(i); + if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; + + PushArrayCell(hPlayers, i); + } + + new iPlayerCount = GetArraySize(hPlayers); + if (iPlayerCount) + { + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + + new Handle:hMenu = CreateMenu(Menu_ViewGroupMembersQueue); + SetMenuTitle(hMenu, "%t%T (%s)\n \n", "SF2 Prefix", "SF2 View Group Members Menu Title", client, sGroupName); + + decl String:sUserId[32]; + decl String:sName[MAX_NAME_LENGTH * 2]; + + for (new i = 0; i < iPlayerCount; i++) + { + new iClient = GetArrayCell(hPlayers, i); + IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); + GetClientName(iClient, sName, sizeof(sName)); + if (GetPlayerGroupLeader(iGroupIndex) == iClient) StrCat(sName, sizeof(sName), " (LEADER)"); + + AddMenuItem(hMenu, sUserId, sName); + } + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + } + else + { + // No players! + DisplayQueuePointsMenu(client); + } + + CloseHandle(hPlayers); +} + +public Menu_ViewGroupMembersQueue(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_End: CloseHandle(menu); + case MenuAction_Select: DisplayQueuePointsMenu(param1); + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack) DisplayQueuePointsMenu(param1); + } + } +} + +DisplayResetQueuePointsMenu(client) +{ + decl String:buffer[256]; + + new Handle:menu = CreateMenu(Menu_ResetQueuePoints); + Format(buffer, sizeof(buffer), "%T", "Yes", client); + AddMenuItem(menu, "0", buffer); + Format(buffer, sizeof(buffer), "%T", "No", client); + AddMenuItem(menu, "1", buffer); + SetMenuTitle(menu, "%T\n \n", "SF2 Should Reset Queue Points", client); + DisplayMenu(menu, client, MENU_TIME_FOREVER); +} + +public Menu_QueuePoints(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_Select: + { + new String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + + if (StrEqual(sInfo, "ponyponypony")) DisplayResetQueuePointsMenu(param1); + else if (!StrContains(sInfo, "player_")) + { + } + else if (!StrContains(sInfo, "group_")) + { + decl String:sIndex[64]; + strcopy(sIndex, sizeof(sIndex), sInfo); + ReplaceString(sIndex, sizeof(sIndex), "group_", ""); + DisplayViewGroupMembersQueueMenu(param1, StringToInt(sIndex)); + } + } + case MenuAction_Cancel: + { + if (param2 == MenuCancel_ExitBack) + { + DisplayMenu(g_hMenuMain, param1, 30); + } + } + case MenuAction_End: CloseHandle(menu); + } +} + +public Menu_ResetQueuePoints(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_Select: + { + switch (param2) + { + case 0: + { + ClientSetQueuePoints(param1, 0); + CPrintToChat(param1, "{olive}%T", "SF2 Queue Points Reset", param1); + + // Special round. + if (IsSpecialRoundRunning()) + { + SetClientPlaySpecialRoundState(param1, true); + } + + // new boss round + if (IsNewBossRoundRunning()) + { + // If the player resets the queue points ignore them when checking for players that haven't played the new boss yet, if applicable. + SetClientPlayNewBossRoundState(param1, true); + } + } + } + + DisplayQueuePointsMenu(param1); + } + + case MenuAction_End: CloseHandle(menu); + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/nav.sp b/addons/sourcemod/scripting/rytp_horror/nav.sp index 3263ad9..609fc28 100644 --- a/addons/sourcemod/scripting/rytp_horror/nav.sp +++ b/addons/sourcemod/scripting/rytp_horror/nav.sp @@ -1,708 +1,708 @@ -#if defined _sf2_nav_included - #endinput -#endif -#define _sf2_nav_included - -#define JumpCrouchHeight 58.0 - -#if defined METHODMAPS - -methodmap NavPath < Handle -{ - public NavPath() - { - return NavPath:CreateNavPath(); - } - - public int AddNodeToHead(float nodePos[3]) - { - return NavPathAddNodeToHead(this, nodePos); - } - - public int AddNodeToTail(float nodePos[3]) - { - return NavPathAddNodeToTail(this, nodePos); - } - - public void GetNodePosition(int nodeIndex, float buffer[3]) - { - NavPathGetNodePosition(this, nodeIndex, buffer); - } - - public int GetNodeAreaIndex(int nodeIndex) - { - return NavPathGetNodeAreaIndex(this, nodeIndex); - } - - public int GetNodeLadderIndex(int nodeIndex) - { - return NavPathGetNodeLadderIndex(this, nodeIndex); - } - - public bool ConstructPathFromPoints(float startPos[3], float endPos[3], float nearestAreaRadius, Function costFunction, any costData, bool populateIfIncomplete = true, int &closestAreaIndex = -1) - { - return NavPathConstructPathFromPoints(this, startPos, endPos, nearestAreaRadius, costFunction, costData, populateIfIncomplete, closestAreaIndex); - } -} - -#endif - -stock Handle:CreateNavPath() -{ - return CreateArray(5); -} - -stock NavPathGetNodePosition(Handle:hNavPath, iNodeIndex, Float:buffer[3]) -{ - buffer[0] = Float:GetArrayCell(hNavPath, iNodeIndex, 0); - buffer[1] = Float:GetArrayCell(hNavPath, iNodeIndex, 1); - buffer[2] = Float:GetArrayCell(hNavPath, iNodeIndex, 2); -} - -stock NavPathGetNodeAreaIndex(Handle:hNavPath, iNodeIndex) -{ - return GetArrayCell(hNavPath, iNodeIndex, 3); -} - -stock NavPathGetNodeLadderIndex(Handle:hNavPath, iNodeIndex) -{ - return GetArrayCell(hNavPath, iNodeIndex, 4); -} - -stock NavPathAddNodeToHead(Handle:hNavPath, const Float:flNodePos[3], iNodeAreaIndex, iLadderIndex=-1) -{ - new iIndex = -1; - - if (GetArraySize(hNavPath) == 0) - { - iIndex = PushArrayArray(hNavPath, flNodePos, 3); - - } - else - { - iIndex = 0; - ShiftArrayUp(hNavPath, 0); - SetArrayArray(hNavPath, iIndex, flNodePos, 3); - } - - SetArrayCell(hNavPath, iIndex, iNodeAreaIndex, 3); - SetArrayCell(hNavPath, iIndex, iLadderIndex, 4); - - return iIndex; -} - -stock NavPathAddNodeToTail(Handle:hNavPath, const Float:flNodePos[3], iNodeAreaIndex, iLadderIndex=-1) -{ - new iIndex = PushArrayArray(hNavPath, flNodePos, 3); - SetArrayCell(hNavPath, iIndex, iNodeAreaIndex, 3); - SetArrayCell(hNavPath, iIndex, iLadderIndex, 4); - - return iIndex; -} - -/** - * Constructs a straight path leading from flStartPos to flEndPos. Useful if both points are within the same area, so pathing around is unnecessary. - */ -stock bool:NavPathConstructTrivialPath(Handle:hNavPath, const Float:flStartPos[3], const Float:flEndPos[3], Float:flNearestAreaRadius) -{ - ClearArray(hNavPath); - - new iStartAreaIndex = NavMesh_GetNearestArea(flStartPos, _, flNearestAreaRadius); - if (iStartAreaIndex == -1) return false; - - new iEndAreaIndex = NavMesh_GetNearestArea(flEndPos, _, flNearestAreaRadius); - if (iEndAreaIndex == -1) return false; - - // Build a trivial path instead. - decl Float:flStartPosOnNavMesh[3]; - flStartPosOnNavMesh[0] = flStartPos[0]; - flStartPosOnNavMesh[1] = flStartPos[1]; - flStartPosOnNavMesh[2] = NavMeshArea_GetZ(iStartAreaIndex, flStartPos); - - NavPathAddNodeToTail(hNavPath, flStartPosOnNavMesh, iStartAreaIndex); - - decl Float:flEndPosOnNavMesh[3]; - flEndPosOnNavMesh[0] = flEndPos[0]; - flEndPosOnNavMesh[1] = flEndPos[1]; - flEndPosOnNavMesh[2] = NavMeshArea_GetZ(iEndAreaIndex, flEndPos); - - NavPathAddNodeToTail(hNavPath, flEndPosOnNavMesh, iEndAreaIndex); - - return true; -} - -/** - * Constructs a path leading from flStartPos to flEndPos. First node index (0) is the start of the path, last node index is the end. - */ -stock bool:NavPathConstructPathFromPoints(Handle:hNavPath, const Float:flStartPos[3], const Float:flEndPos[3], Float:flNearestAreaRadius, Function:fCostFunction, any:iCostData=-1, bool:bPopulateIfIncomplete=false, &iClosestAreaIndex=0) -{ - ClearArray(hNavPath); - - new iStartAreaIndex = NavMesh_GetNearestArea(flStartPos, _, flNearestAreaRadius); - if (iStartAreaIndex == -1) return false; - - new iEndAreaIndex = NavMesh_GetNearestArea(flEndPos, _, flNearestAreaRadius); - if (iEndAreaIndex == -1) return false; - - if (iStartAreaIndex == iEndAreaIndex) - { - return NavPathConstructTrivialPath(hNavPath, flStartPos, flEndPos, flNearestAreaRadius); - } - - iClosestAreaIndex = 0; - - new bool:bResult = NavMesh_BuildPath(iStartAreaIndex, - iEndAreaIndex, - flEndPos, - fCostFunction, - iCostData, - iClosestAreaIndex); - - if (!bResult && bPopulateIfIncomplete) return false; - - if (bResult) - { - // Because we were able to get to the goal position successfully, add the goal position itself. - decl Float:flEndPosOnNavMesh[3]; - flEndPosOnNavMesh[0] = flEndPos[0]; - flEndPosOnNavMesh[1] = flEndPos[1]; - flEndPosOnNavMesh[2] = NavMeshArea_GetZ(iEndAreaIndex, flEndPos); - - NavPathAddNodeToHead(hNavPath, flEndPosOnNavMesh, iEndAreaIndex); - } - - decl Float:flCenter[3], Float:flCenterPortal[3], Float:flClosestPoint[3]; - - new iTempAreaIndex = iClosestAreaIndex; - new iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); - new iNavDirection; - new Float:flHalfWidth; - - while (iTempParentAreaIndex != -1) - { - // Build a path of waypoints along the nav mesh for our AI to follow. - - NavMeshArea_GetCenter(iTempParentAreaIndex, flCenter); - iNavDirection = NavMeshArea_ComputeDirection(iTempAreaIndex, flCenter); - NavMeshArea_ComputePortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flHalfWidth); - NavMeshArea_ComputeClosestPointInPortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flClosestPoint); - - flClosestPoint[2] = NavMeshArea_GetZ(iTempAreaIndex, flClosestPoint); - - NavPathAddNodeToHead(hNavPath, flClosestPoint, iTempAreaIndex); - - iTempAreaIndex = iTempParentAreaIndex; - iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); - } - - decl Float:flStartPosOnNavMesh[3]; - flStartPosOnNavMesh[0] = flStartPos[0]; - flStartPosOnNavMesh[1] = flStartPos[1]; - flStartPosOnNavMesh[2] = NavMeshArea_GetZ(iStartAreaIndex, flStartPos); - - NavPathAddNodeToHead(hNavPath, flStartPosOnNavMesh, iStartAreaIndex); - - return bResult; -} - -/** - * Return the closest point to our current position on our current path - * If "local" is true, only check the portion of the path surrounding iPathNodeIndex. - * (function imported from HL SDK) - */ -stock FindClosestPositionOnPath(Handle:hNavPath, const Float:flFeetPos[3], const Float:flCentroidPos[3], const Float:flEyePos[3], Float:flBuffer[3]=NULL_VECTOR, bool:bLocal=false, iPathNodeIndex=-1) -{ - if (hNavPath == INVALID_HANDLE) return -1; - - new iNodeCount = GetArraySize(hNavPath); - if (iNodeCount == 0) return -1; - - new iStartNode = -1; - new iEndNode = -1; - - if (bLocal) - { - // Clamp nodes to stay within path segment. - iStartNode = iPathNodeIndex - 3; - if (iStartNode < 1) iStartNode = 1; - - iEndNode = iPathNodeIndex + 3; - if (iEndNode > iNodeCount) iEndNode = iNodeCount; - } - else - { - iStartNode = 1; - iEndNode = iNodeCount; - } - - decl Float:flFrom[3], Float:flTo[3]; - decl Float:flAlong[3], Float:flToFeetPos[3]; - - decl Float:flLength, Float:flCloseLength, Float:flDistSq; - - decl Float:flPos[3], Float:flSub[3], Float:flProbe[3]; - - new Float:flCloseDistSq = 9999999999.9; - new iCloseIndex = -1; - - new Float:flMidHeight = flCentroidPos[2] - flFeetPos[2]; - - for (new i = iStartNode; i < iEndNode; i++) - { - NavPathGetNodePosition(hNavPath, i - 1, flFrom); - NavPathGetNodePosition(hNavPath, i, flTo); - - // Convert flAlong to unit vector. - SubtractVectors(flTo, flFrom, flAlong); - flLength = GetVectorLength(flAlong); - NormalizeVector(flAlong, flAlong); - - SubtractVectors(flFeetPos, flFrom, flToFeetPos); - - // Clamp point onto current path segment. - flCloseLength = GetVectorDotProduct(flToFeetPos, flAlong); - if (flCloseLength <= 0.0) - { - flPos[0] = flFrom[0]; - flPos[1] = flFrom[1]; - flPos[2] = flFrom[2]; - } - else if (flCloseLength >= flLength) - { - flPos[0] = flTo[0]; - flPos[1] = flTo[1]; - flPos[2] = flTo[2]; - } - else - { - flPos[0] = flFrom[0] + (flCloseLength * flAlong[0]); - flPos[1] = flFrom[1] + (flCloseLength * flAlong[1]); - flPos[2] = flFrom[2] + (flCloseLength * flAlong[2]); - } - - SubtractVectors(flPos, flFeetPos, flSub); - flDistSq = GetVectorLength(flSub, true); - - if (flDistSq < flCloseDistSq) - { - flProbe[0] = flPos[0]; - flProbe[1] = flPos[1]; - flProbe[2] = flPos[2] + flMidHeight; - - if (!IsWalkableTraceLineClear(flEyePos, flProbe, WALK_THRU_DOORS | WALK_THRU_BREAKABLES)) continue; - - flCloseDistSq = flDistSq; - CopyVector(flPos, flBuffer); - - iCloseIndex = i - 1; - } - } - - return iCloseIndex; -} - -/** - * Computes a point a fixed distance ahead of our path. - * Returns path index just after point. - * (function imported from HL SDK) - */ -stock FindAheadPathPoint(Handle:hNavPath, Float:flAheadRange, iPathNodeIndex, const Float:flFeetPos[3], const Float:flCentroidPos[3], const Float:flEyePos[3], Float:flPoint[3], &iPrevPathNodeIndex) -{ - if (hNavPath == INVALID_HANDLE) return -1; - - new iAfterPathNodeIndex; - - decl Float:flClosestPos[3]; - - new iStartPathNodeIndex = FindClosestPositionOnPath(hNavPath, flFeetPos, flCentroidPos, flEyePos, flClosestPos, true, iPathNodeIndex); - iPrevPathNodeIndex = iStartPathNodeIndex; - - if (iStartPathNodeIndex <= 0) - { - // Went off the end of the path or next point in path is unwalkable (ie: jump-down). Keep same point - return iPathNodeIndex; - } - - decl Float:flFeetPos2D[3], Float:flPathNodePos2D[3]; - CopyVector(flFeetPos, flFeetPos2D); - flFeetPos2D[2] = 0.0; - - while (iStartPathNodeIndex < (GetArraySize(hNavPath) - 1)) - { - decl Float:flPathNodePos[3]; - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPathNodePos); - flPathNodePos2D[2] = 0.0; - - static Float:closeEpsilon = 20.0; - - if (GetVectorDistance(flFeetPos2D, flPathNodePos2D) < closeEpsilon) - { - iStartPathNodeIndex++; - } - else - { - break; - } - } - - // Approaching jump area? Look no further, we must stop here. - if (iStartPathNodeIndex > iPathNodeIndex && iStartPathNodeIndex < GetArraySize(hNavPath) && - NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, iStartPathNodeIndex)) & NAV_MESH_JUMP) - { - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); - return iStartPathNodeIndex; - } - - iStartPathNodeIndex++; - - // Approaching jump area? Look no further, we must stop here. - if (iStartPathNodeIndex < GetArraySize(hNavPath) && - NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, iStartPathNodeIndex)) & NAV_MESH_JUMP) - { - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); - return iStartPathNodeIndex; - } - - // Get the direction of the path segment we're currently on. - decl Float:flStartPathNodePos[3], Float:flPrevStartPathNodePos[3]; - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flStartPathNodePos); - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex - 1, flPrevStartPathNodePos); - - decl Float:flInitDir[3]; - SubtractVectors(flStartPathNodePos, flPrevStartPathNodePos, flInitDir); - NormalizeVector(flInitDir, flInitDir); - - new Float:flRangeSoFar = 0.0; - - // bVisible is true if our ahead point is visible. - new bool:bVisible = true; - - decl Float:flPrevDir[3]; - CopyVector(flInitDir, flPrevDir); - - new bool:bIsCorner = false; - new i = 0; - - new Float:flMidHeight = flCentroidPos[2] - flFeetPos[2]; - - // Step along the path until we pass flAheadRange. - for (i = iStartPathNodeIndex; i < GetArraySize(hNavPath); i++) - { - decl Float:flPathNodePos[3], Float:flTo[3], Float:flDir[3]; - NavPathGetNodePosition(hNavPath, i, flPathNodePos); - NavPathGetNodePosition(hNavPath, i - 1, flTo); - NegateVector(flTo); - AddVectors(flPathNodePos, flTo, flTo); - - NormalizeVector(flTo, flDir); - - if (GetVectorDotProduct(flDir, flInitDir) < 0.0) - { - // Don't double back. - i--; - break; - } - - if (GetVectorDotProduct(flDir, flPrevDir) < 0.0) - { - // Don't cut corners. - bIsCorner = true; - i--; - break; - } - - CopyVector(flDir, flPrevDir); - - decl Float:flProbe[3]; - CopyVector(flPathNodePos, flProbe); - flProbe[2] += flMidHeight; - - if (!IsWalkableTraceLineClear( flEyePos, flProbe, WALK_THRU_BREAKABLES )) - { - // Points aren't visible ahead; stick to the last visible point ahead. - bVisible = false; - break; - } - - if (NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, i)) & NAV_MESH_JUMP) - { - // Jump area here; stop. - break; - } - - if (i == iStartPathNodeIndex) - { - decl Float:flAlong[3]; - SubtractVectors(flPathNodePos, flFeetPos, flAlong); - flAlong[2] = 0.0; - flRangeSoFar += GetVectorLength(flAlong); - } - else - { - flRangeSoFar += GetVectorLength(flTo); - } - - if (flRangeSoFar >= flAheadRange) - { - // Went ahead of flAheadRange; stop. - break; - } - } - - // clamp iAfterPathNodeIndex between starting path node and the end - if (i < iStartPathNodeIndex) - { - iAfterPathNodeIndex = iStartPathNodeIndex; - } - else if (i < GetArraySize(hNavPath)) - { - iAfterPathNodeIndex = i; - } - else - { - iAfterPathNodeIndex = GetArraySize(hNavPath) - 1; - } - - if (iAfterPathNodeIndex == 0) - { - NavPathGetNodePosition(hNavPath, 0, flPoint); - } - else - { - // Interpolate point along path segment to get exact distance. - decl Float:flBeforePointPos[3], Float:flAfterPointPos[3]; - NavPathGetNodePosition(hNavPath, iAfterPathNodeIndex, flAfterPointPos); - NavPathGetNodePosition(hNavPath, iAfterPathNodeIndex - 1, flBeforePointPos); - - decl Float:flTo[3], Float:flTo2D[3]; - SubtractVectors(flAfterPointPos, flBeforePointPos, flTo); - CopyVector(flTo, flTo2D); - flTo2D[2] = 0.0; - - new Float:flLength = GetVectorLength(flTo2D); - new Float:t = 1.0 - ((flRangeSoFar - flAheadRange) / flLength); - - if (t < 0.0) t = 0.0; - else if (t > 1.0) t = 1.0; - - for (new i2 = 0; i2 < 3; i2++) - { - flPoint[i2] = flBeforePointPos[i2] + (t * flTo[i2]); - } - - if (!bVisible) - { - // iAfterPathNodeIndex isn't visible, so slide back towards previous node until it is. - - static const Float:flSightStepSize = 25.0; - new Float:dt = flSightStepSize / flLength; - - decl Float:flProbe[3]; - CopyVector(flPoint, flProbe); - flProbe[2] += flMidHeight; - - while (t > 0.0 && !IsWalkableTraceLineClear(flEyePos, flProbe, WALK_THRU_BREAKABLES)) - { - t -= dt; - - for (new i2 = 0; i2 < 3; i2++) - { - flPoint[i2] = flBeforePointPos[i2] + (t * flTo[i2]); - } - } - - if (t <= 0.0) - { - CopyVector(flBeforePointPos, flPoint); - } - } - } - - // Is there a corner ahead? - if (!bIsCorner) - { - // If position found is behind us or it's too close to us, force it farther down the path so we don't stop and wiggle. - - static const Float:epsilon = 50.0; - - decl Float:flCentroid2D[3]; - CopyVector(flCentroidPos, flCentroid2D); - flCentroid2D[2] = 0.0; - - decl Float:flTo2D[3]; - flTo2D[0] = flPoint[0] - flCentroid2D[0]; - flTo2D[1] = flPoint[1] - flCentroid2D[1]; - flTo2D[2] = 0.0; - - decl Float:flInitDir2D[3]; - CopyVector(flInitDir, flInitDir2D); - flInitDir2D[2] = 0.0; - - if (GetVectorDotProduct(flTo2D, flInitDir2D) < 0.0 || GetVectorLength(flTo2D) < epsilon) - { - // Check points ahead. - for (i = iStartPathNodeIndex; i < GetArraySize(hNavPath); i++) - { - decl Float:flPathNodePos[3]; - NavPathGetNodePosition(hNavPath, i, flPathNodePos); - - flTo2D[0] = flPathNodePos[0] - flCentroid2D[0]; - flTo2D[1] = flPathNodePos[1] - flCentroid2D[1]; - - // Check if the point ahead is either a jump/ladder area or is far enough. - if (NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, i)) & NAV_MESH_JUMP || GetVectorLength(flTo2D) > epsilon) - { - CopyVector(flPathNodePos, flPoint); - iStartPathNodeIndex = i; - break; - } - } - - if (i == GetArraySize(hNavPath)) - { - iStartPathNodeIndex = GetArraySize(hNavPath) - 1; - NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); - } - } - } - - if (iStartPathNodeIndex < GetArraySize(hNavPath)) - { - return iStartPathNodeIndex; - } - - return GetArraySize(hNavPath) - 1; -} - - -stock CalculateFeelerReflexAdjustment(const Float:flOriginalMovePos[3], - const Float:flOriginalFeetPos[3], - const Float:flFloorNormalDir[3], - Float:flFeelerHeight, - Float:flFeelerOffset, - Float:flFeelerLength, - Float:flAvoidRange, - Float:flBuffer[3], - iTraceMask=MASK_PLAYERSOLID, - Function:fTraceFilterFunction=INVALID_FUNCTION, - any:iTraceFilterFunctionData=-1) -{ - // Forward direction vector. - decl Float:flOriginalMoveDir[3], Float:flLateralDir[3]; - SubtractVectors(flOriginalMovePos, flOriginalFeetPos, flOriginalMoveDir); - flOriginalMoveDir[2] = 0.0; - - GetVectorAngles(flOriginalMoveDir, flOriginalMoveDir); - GetAngleVectors(flOriginalMoveDir, flOriginalMoveDir, flLateralDir, NULL_VECTOR); - NormalizeVector(flOriginalMoveDir, flOriginalMoveDir); - NormalizeVector(flLateralDir, flLateralDir); - NegateVector(flLateralDir); - - // Correct move direction vector along floor. - decl Float:flDir[3]; - GetVectorCrossProduct(flLateralDir, flFloorNormalDir, flDir); - NormalizeVector(flDir, flDir); - - // Correct lateral direction vector along floor. - GetVectorCrossProduct(flDir, flFloorNormalDir, flLateralDir); - NormalizeVector(flLateralDir, flLateralDir); - - if (flFeelerHeight <= 0.0) - { - flFeelerHeight = StepHeight + 0.1; - } - - decl Float:flFeetPos[3]; - CopyVector(flOriginalFeetPos, flFeetPos); - flFeetPos[2] += flFeelerHeight; - - decl Float:flFromPos[3]; - decl Float:flToPos[3]; - - // Check the left. - for (new i = 0; i < 3; i++) - { - flFromPos[i] = flFeetPos[i] + (flFeelerOffset * flLateralDir[i]); - flToPos[i] = flFromPos[i] + (flFeelerLength * flDir[i]); - } - - new Handle:hTrace = INVALID_HANDLE; - if (fTraceFilterFunction != INVALID_FUNCTION) - { - hTrace = TR_TraceRayFilterEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint, fTraceFilterFunction, iTraceFilterFunctionData); - } - else - { - hTrace = TR_TraceRayEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint); - } - - new bool:bLeftClear = !TR_DidHit(hTrace); - CloseHandle(hTrace); - -#if defined DEBUG - - if (bLeftClear) - { - TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 0, 255, 0, 255 }, 30); - } - else - { - TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 255, 0, 0, 255 }, 30); - } - - TE_SendToAll(); - -#endif - - // Check the right. - for (new i = 0; i < 3; i++) - { - flFromPos[i] = flFeetPos[i] - (flFeelerOffset * flLateralDir[i]); - flToPos[i] = flFromPos[i] + (flFeelerLength * flDir[i]); - } - - if (fTraceFilterFunction != INVALID_FUNCTION) - { - hTrace = TR_TraceRayFilterEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint, fTraceFilterFunction, iTraceFilterFunctionData); - } - else - { - hTrace = TR_TraceRayEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint); - } - - new bool:bRightClear = !TR_DidHit(hTrace); - CloseHandle(hTrace); - -#if defined DEBUG - - if (bRightClear) - { - TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 0, 255, 0, 255 }, 30); - } - else - { - TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 255, 0, 0, 255 }, 30); - } - - TE_SendToAll(); - -#endif - - if (!bRightClear) - { - if (bLeftClear) - { - for (new i = 0; i < 3; i++) - { - flBuffer[i] = flOriginalMovePos[i] + (flAvoidRange * flLateralDir[i]); - } - } - } - else if (!bLeftClear) - { - for (new i = 0; i < 3; i++) - { - flBuffer[i] = flOriginalMovePos[i] - (flAvoidRange * flLateralDir[i]); - } - } +#if defined _sf2_nav_included + #endinput +#endif +#define _sf2_nav_included + +#define JumpCrouchHeight 58.0 + +#if defined METHODMAPS + +methodmap NavPath < Handle +{ + public NavPath() + { + return NavPath:CreateNavPath(); + } + + public int AddNodeToHead(float nodePos[3]) + { + return NavPathAddNodeToHead(this, nodePos); + } + + public int AddNodeToTail(float nodePos[3]) + { + return NavPathAddNodeToTail(this, nodePos); + } + + public void GetNodePosition(int nodeIndex, float buffer[3]) + { + NavPathGetNodePosition(this, nodeIndex, buffer); + } + + public int GetNodeAreaIndex(int nodeIndex) + { + return NavPathGetNodeAreaIndex(this, nodeIndex); + } + + public int GetNodeLadderIndex(int nodeIndex) + { + return NavPathGetNodeLadderIndex(this, nodeIndex); + } + + public bool ConstructPathFromPoints(float startPos[3], float endPos[3], float nearestAreaRadius, Function costFunction, any costData, bool populateIfIncomplete = true, int &closestAreaIndex = -1) + { + return NavPathConstructPathFromPoints(this, startPos, endPos, nearestAreaRadius, costFunction, costData, populateIfIncomplete, closestAreaIndex); + } +} + +#endif + +stock Handle:CreateNavPath() +{ + return CreateArray(5); +} + +stock NavPathGetNodePosition(Handle:hNavPath, iNodeIndex, Float:buffer[3]) +{ + buffer[0] = Float:GetArrayCell(hNavPath, iNodeIndex, 0); + buffer[1] = Float:GetArrayCell(hNavPath, iNodeIndex, 1); + buffer[2] = Float:GetArrayCell(hNavPath, iNodeIndex, 2); +} + +stock NavPathGetNodeAreaIndex(Handle:hNavPath, iNodeIndex) +{ + return GetArrayCell(hNavPath, iNodeIndex, 3); +} + +stock NavPathGetNodeLadderIndex(Handle:hNavPath, iNodeIndex) +{ + return GetArrayCell(hNavPath, iNodeIndex, 4); +} + +stock NavPathAddNodeToHead(Handle:hNavPath, const Float:flNodePos[3], iNodeAreaIndex, iLadderIndex=-1) +{ + new iIndex = -1; + + if (GetArraySize(hNavPath) == 0) + { + iIndex = PushArrayArray(hNavPath, flNodePos, 3); + + } + else + { + iIndex = 0; + ShiftArrayUp(hNavPath, 0); + SetArrayArray(hNavPath, iIndex, flNodePos, 3); + } + + SetArrayCell(hNavPath, iIndex, iNodeAreaIndex, 3); + SetArrayCell(hNavPath, iIndex, iLadderIndex, 4); + + return iIndex; +} + +stock NavPathAddNodeToTail(Handle:hNavPath, const Float:flNodePos[3], iNodeAreaIndex, iLadderIndex=-1) +{ + new iIndex = PushArrayArray(hNavPath, flNodePos, 3); + SetArrayCell(hNavPath, iIndex, iNodeAreaIndex, 3); + SetArrayCell(hNavPath, iIndex, iLadderIndex, 4); + + return iIndex; +} + +/** + * Constructs a straight path leading from flStartPos to flEndPos. Useful if both points are within the same area, so pathing around is unnecessary. + */ +stock bool:NavPathConstructTrivialPath(Handle:hNavPath, const Float:flStartPos[3], const Float:flEndPos[3], Float:flNearestAreaRadius) +{ + ClearArray(hNavPath); + + new iStartAreaIndex = NavMesh_GetNearestArea(flStartPos, _, flNearestAreaRadius); + if (iStartAreaIndex == -1) return false; + + new iEndAreaIndex = NavMesh_GetNearestArea(flEndPos, _, flNearestAreaRadius); + if (iEndAreaIndex == -1) return false; + + // Build a trivial path instead. + decl Float:flStartPosOnNavMesh[3]; + flStartPosOnNavMesh[0] = flStartPos[0]; + flStartPosOnNavMesh[1] = flStartPos[1]; + flStartPosOnNavMesh[2] = NavMeshArea_GetZ(iStartAreaIndex, flStartPos); + + NavPathAddNodeToTail(hNavPath, flStartPosOnNavMesh, iStartAreaIndex); + + decl Float:flEndPosOnNavMesh[3]; + flEndPosOnNavMesh[0] = flEndPos[0]; + flEndPosOnNavMesh[1] = flEndPos[1]; + flEndPosOnNavMesh[2] = NavMeshArea_GetZ(iEndAreaIndex, flEndPos); + + NavPathAddNodeToTail(hNavPath, flEndPosOnNavMesh, iEndAreaIndex); + + return true; +} + +/** + * Constructs a path leading from flStartPos to flEndPos. First node index (0) is the start of the path, last node index is the end. + */ +stock bool:NavPathConstructPathFromPoints(Handle:hNavPath, const Float:flStartPos[3], const Float:flEndPos[3], Float:flNearestAreaRadius, Function:fCostFunction, any:iCostData=-1, bool:bPopulateIfIncomplete=false, &iClosestAreaIndex=0) +{ + ClearArray(hNavPath); + + new iStartAreaIndex = NavMesh_GetNearestArea(flStartPos, _, flNearestAreaRadius); + if (iStartAreaIndex == -1) return false; + + new iEndAreaIndex = NavMesh_GetNearestArea(flEndPos, _, flNearestAreaRadius); + if (iEndAreaIndex == -1) return false; + + if (iStartAreaIndex == iEndAreaIndex) + { + return NavPathConstructTrivialPath(hNavPath, flStartPos, flEndPos, flNearestAreaRadius); + } + + iClosestAreaIndex = 0; + + new bool:bResult = NavMesh_BuildPath(iStartAreaIndex, + iEndAreaIndex, + flEndPos, + fCostFunction, + iCostData, + iClosestAreaIndex); + + if (!bResult && bPopulateIfIncomplete) return false; + + if (bResult) + { + // Because we were able to get to the goal position successfully, add the goal position itself. + decl Float:flEndPosOnNavMesh[3]; + flEndPosOnNavMesh[0] = flEndPos[0]; + flEndPosOnNavMesh[1] = flEndPos[1]; + flEndPosOnNavMesh[2] = NavMeshArea_GetZ(iEndAreaIndex, flEndPos); + + NavPathAddNodeToHead(hNavPath, flEndPosOnNavMesh, iEndAreaIndex); + } + + decl Float:flCenter[3], Float:flCenterPortal[3], Float:flClosestPoint[3]; + + new iTempAreaIndex = iClosestAreaIndex; + new iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); + new iNavDirection; + new Float:flHalfWidth; + + while (iTempParentAreaIndex != -1) + { + // Build a path of waypoints along the nav mesh for our AI to follow. + + NavMeshArea_GetCenter(iTempParentAreaIndex, flCenter); + iNavDirection = NavMeshArea_ComputeDirection(iTempAreaIndex, flCenter); + NavMeshArea_ComputePortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flHalfWidth); + NavMeshArea_ComputeClosestPointInPortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flClosestPoint); + + flClosestPoint[2] = NavMeshArea_GetZ(iTempAreaIndex, flClosestPoint); + + NavPathAddNodeToHead(hNavPath, flClosestPoint, iTempAreaIndex); + + iTempAreaIndex = iTempParentAreaIndex; + iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); + } + + decl Float:flStartPosOnNavMesh[3]; + flStartPosOnNavMesh[0] = flStartPos[0]; + flStartPosOnNavMesh[1] = flStartPos[1]; + flStartPosOnNavMesh[2] = NavMeshArea_GetZ(iStartAreaIndex, flStartPos); + + NavPathAddNodeToHead(hNavPath, flStartPosOnNavMesh, iStartAreaIndex); + + return bResult; +} + +/** + * Return the closest point to our current position on our current path + * If "local" is true, only check the portion of the path surrounding iPathNodeIndex. + * (function imported from HL SDK) + */ +stock FindClosestPositionOnPath(Handle:hNavPath, const Float:flFeetPos[3], const Float:flCentroidPos[3], const Float:flEyePos[3], Float:flBuffer[3]=NULL_VECTOR, bool:bLocal=false, iPathNodeIndex=-1) +{ + if (hNavPath == INVALID_HANDLE) return -1; + + new iNodeCount = GetArraySize(hNavPath); + if (iNodeCount == 0) return -1; + + new iStartNode = -1; + new iEndNode = -1; + + if (bLocal) + { + // Clamp nodes to stay within path segment. + iStartNode = iPathNodeIndex - 3; + if (iStartNode < 1) iStartNode = 1; + + iEndNode = iPathNodeIndex + 3; + if (iEndNode > iNodeCount) iEndNode = iNodeCount; + } + else + { + iStartNode = 1; + iEndNode = iNodeCount; + } + + decl Float:flFrom[3], Float:flTo[3]; + decl Float:flAlong[3], Float:flToFeetPos[3]; + + decl Float:flLength, Float:flCloseLength, Float:flDistSq; + + decl Float:flPos[3], Float:flSub[3], Float:flProbe[3]; + + new Float:flCloseDistSq = 9999999999.9; + new iCloseIndex = -1; + + new Float:flMidHeight = flCentroidPos[2] - flFeetPos[2]; + + for (new i = iStartNode; i < iEndNode; i++) + { + NavPathGetNodePosition(hNavPath, i - 1, flFrom); + NavPathGetNodePosition(hNavPath, i, flTo); + + // Convert flAlong to unit vector. + SubtractVectors(flTo, flFrom, flAlong); + flLength = GetVectorLength(flAlong); + NormalizeVector(flAlong, flAlong); + + SubtractVectors(flFeetPos, flFrom, flToFeetPos); + + // Clamp point onto current path segment. + flCloseLength = GetVectorDotProduct(flToFeetPos, flAlong); + if (flCloseLength <= 0.0) + { + flPos[0] = flFrom[0]; + flPos[1] = flFrom[1]; + flPos[2] = flFrom[2]; + } + else if (flCloseLength >= flLength) + { + flPos[0] = flTo[0]; + flPos[1] = flTo[1]; + flPos[2] = flTo[2]; + } + else + { + flPos[0] = flFrom[0] + (flCloseLength * flAlong[0]); + flPos[1] = flFrom[1] + (flCloseLength * flAlong[1]); + flPos[2] = flFrom[2] + (flCloseLength * flAlong[2]); + } + + SubtractVectors(flPos, flFeetPos, flSub); + flDistSq = GetVectorLength(flSub, true); + + if (flDistSq < flCloseDistSq) + { + flProbe[0] = flPos[0]; + flProbe[1] = flPos[1]; + flProbe[2] = flPos[2] + flMidHeight; + + if (!IsWalkableTraceLineClear(flEyePos, flProbe, WALK_THRU_DOORS | WALK_THRU_BREAKABLES)) continue; + + flCloseDistSq = flDistSq; + CopyVector(flPos, flBuffer); + + iCloseIndex = i - 1; + } + } + + return iCloseIndex; +} + +/** + * Computes a point a fixed distance ahead of our path. + * Returns path index just after point. + * (function imported from HL SDK) + */ +stock FindAheadPathPoint(Handle:hNavPath, Float:flAheadRange, iPathNodeIndex, const Float:flFeetPos[3], const Float:flCentroidPos[3], const Float:flEyePos[3], Float:flPoint[3], &iPrevPathNodeIndex) +{ + if (hNavPath == INVALID_HANDLE) return -1; + + new iAfterPathNodeIndex; + + decl Float:flClosestPos[3]; + + new iStartPathNodeIndex = FindClosestPositionOnPath(hNavPath, flFeetPos, flCentroidPos, flEyePos, flClosestPos, true, iPathNodeIndex); + iPrevPathNodeIndex = iStartPathNodeIndex; + + if (iStartPathNodeIndex <= 0) + { + // Went off the end of the path or next point in path is unwalkable (ie: jump-down). Keep same point + return iPathNodeIndex; + } + + decl Float:flFeetPos2D[3], Float:flPathNodePos2D[3]; + CopyVector(flFeetPos, flFeetPos2D); + flFeetPos2D[2] = 0.0; + + while (iStartPathNodeIndex < (GetArraySize(hNavPath) - 1)) + { + decl Float:flPathNodePos[3]; + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPathNodePos); + flPathNodePos2D[2] = 0.0; + + static Float:closeEpsilon = 20.0; + + if (GetVectorDistance(flFeetPos2D, flPathNodePos2D) < closeEpsilon) + { + iStartPathNodeIndex++; + } + else + { + break; + } + } + + // Approaching jump area? Look no further, we must stop here. + if (iStartPathNodeIndex > iPathNodeIndex && iStartPathNodeIndex < GetArraySize(hNavPath) && + NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, iStartPathNodeIndex)) & NAV_MESH_JUMP) + { + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); + return iStartPathNodeIndex; + } + + iStartPathNodeIndex++; + + // Approaching jump area? Look no further, we must stop here. + if (iStartPathNodeIndex < GetArraySize(hNavPath) && + NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, iStartPathNodeIndex)) & NAV_MESH_JUMP) + { + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); + return iStartPathNodeIndex; + } + + // Get the direction of the path segment we're currently on. + decl Float:flStartPathNodePos[3], Float:flPrevStartPathNodePos[3]; + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flStartPathNodePos); + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex - 1, flPrevStartPathNodePos); + + decl Float:flInitDir[3]; + SubtractVectors(flStartPathNodePos, flPrevStartPathNodePos, flInitDir); + NormalizeVector(flInitDir, flInitDir); + + new Float:flRangeSoFar = 0.0; + + // bVisible is true if our ahead point is visible. + new bool:bVisible = true; + + decl Float:flPrevDir[3]; + CopyVector(flInitDir, flPrevDir); + + new bool:bIsCorner = false; + new i = 0; + + new Float:flMidHeight = flCentroidPos[2] - flFeetPos[2]; + + // Step along the path until we pass flAheadRange. + for (i = iStartPathNodeIndex; i < GetArraySize(hNavPath); i++) + { + decl Float:flPathNodePos[3], Float:flTo[3], Float:flDir[3]; + NavPathGetNodePosition(hNavPath, i, flPathNodePos); + NavPathGetNodePosition(hNavPath, i - 1, flTo); + NegateVector(flTo); + AddVectors(flPathNodePos, flTo, flTo); + + NormalizeVector(flTo, flDir); + + if (GetVectorDotProduct(flDir, flInitDir) < 0.0) + { + // Don't double back. + i--; + break; + } + + if (GetVectorDotProduct(flDir, flPrevDir) < 0.0) + { + // Don't cut corners. + bIsCorner = true; + i--; + break; + } + + CopyVector(flDir, flPrevDir); + + decl Float:flProbe[3]; + CopyVector(flPathNodePos, flProbe); + flProbe[2] += flMidHeight; + + if (!IsWalkableTraceLineClear( flEyePos, flProbe, WALK_THRU_BREAKABLES )) + { + // Points aren't visible ahead; stick to the last visible point ahead. + bVisible = false; + break; + } + + if (NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, i)) & NAV_MESH_JUMP) + { + // Jump area here; stop. + break; + } + + if (i == iStartPathNodeIndex) + { + decl Float:flAlong[3]; + SubtractVectors(flPathNodePos, flFeetPos, flAlong); + flAlong[2] = 0.0; + flRangeSoFar += GetVectorLength(flAlong); + } + else + { + flRangeSoFar += GetVectorLength(flTo); + } + + if (flRangeSoFar >= flAheadRange) + { + // Went ahead of flAheadRange; stop. + break; + } + } + + // clamp iAfterPathNodeIndex between starting path node and the end + if (i < iStartPathNodeIndex) + { + iAfterPathNodeIndex = iStartPathNodeIndex; + } + else if (i < GetArraySize(hNavPath)) + { + iAfterPathNodeIndex = i; + } + else + { + iAfterPathNodeIndex = GetArraySize(hNavPath) - 1; + } + + if (iAfterPathNodeIndex == 0) + { + NavPathGetNodePosition(hNavPath, 0, flPoint); + } + else + { + // Interpolate point along path segment to get exact distance. + decl Float:flBeforePointPos[3], Float:flAfterPointPos[3]; + NavPathGetNodePosition(hNavPath, iAfterPathNodeIndex, flAfterPointPos); + NavPathGetNodePosition(hNavPath, iAfterPathNodeIndex - 1, flBeforePointPos); + + decl Float:flTo[3], Float:flTo2D[3]; + SubtractVectors(flAfterPointPos, flBeforePointPos, flTo); + CopyVector(flTo, flTo2D); + flTo2D[2] = 0.0; + + new Float:flLength = GetVectorLength(flTo2D); + new Float:t = 1.0 - ((flRangeSoFar - flAheadRange) / flLength); + + if (t < 0.0) t = 0.0; + else if (t > 1.0) t = 1.0; + + for (new i2 = 0; i2 < 3; i2++) + { + flPoint[i2] = flBeforePointPos[i2] + (t * flTo[i2]); + } + + if (!bVisible) + { + // iAfterPathNodeIndex isn't visible, so slide back towards previous node until it is. + + static const Float:flSightStepSize = 25.0; + new Float:dt = flSightStepSize / flLength; + + decl Float:flProbe[3]; + CopyVector(flPoint, flProbe); + flProbe[2] += flMidHeight; + + while (t > 0.0 && !IsWalkableTraceLineClear(flEyePos, flProbe, WALK_THRU_BREAKABLES)) + { + t -= dt; + + for (new i2 = 0; i2 < 3; i2++) + { + flPoint[i2] = flBeforePointPos[i2] + (t * flTo[i2]); + } + } + + if (t <= 0.0) + { + CopyVector(flBeforePointPos, flPoint); + } + } + } + + // Is there a corner ahead? + if (!bIsCorner) + { + // If position found is behind us or it's too close to us, force it farther down the path so we don't stop and wiggle. + + static const Float:epsilon = 50.0; + + decl Float:flCentroid2D[3]; + CopyVector(flCentroidPos, flCentroid2D); + flCentroid2D[2] = 0.0; + + decl Float:flTo2D[3]; + flTo2D[0] = flPoint[0] - flCentroid2D[0]; + flTo2D[1] = flPoint[1] - flCentroid2D[1]; + flTo2D[2] = 0.0; + + decl Float:flInitDir2D[3]; + CopyVector(flInitDir, flInitDir2D); + flInitDir2D[2] = 0.0; + + if (GetVectorDotProduct(flTo2D, flInitDir2D) < 0.0 || GetVectorLength(flTo2D) < epsilon) + { + // Check points ahead. + for (i = iStartPathNodeIndex; i < GetArraySize(hNavPath); i++) + { + decl Float:flPathNodePos[3]; + NavPathGetNodePosition(hNavPath, i, flPathNodePos); + + flTo2D[0] = flPathNodePos[0] - flCentroid2D[0]; + flTo2D[1] = flPathNodePos[1] - flCentroid2D[1]; + + // Check if the point ahead is either a jump/ladder area or is far enough. + if (NavMeshArea_GetFlags(NavPathGetNodeAreaIndex(hNavPath, i)) & NAV_MESH_JUMP || GetVectorLength(flTo2D) > epsilon) + { + CopyVector(flPathNodePos, flPoint); + iStartPathNodeIndex = i; + break; + } + } + + if (i == GetArraySize(hNavPath)) + { + iStartPathNodeIndex = GetArraySize(hNavPath) - 1; + NavPathGetNodePosition(hNavPath, iStartPathNodeIndex, flPoint); + } + } + } + + if (iStartPathNodeIndex < GetArraySize(hNavPath)) + { + return iStartPathNodeIndex; + } + + return GetArraySize(hNavPath) - 1; +} + + +stock CalculateFeelerReflexAdjustment(const Float:flOriginalMovePos[3], + const Float:flOriginalFeetPos[3], + const Float:flFloorNormalDir[3], + Float:flFeelerHeight, + Float:flFeelerOffset, + Float:flFeelerLength, + Float:flAvoidRange, + Float:flBuffer[3], + iTraceMask=MASK_PLAYERSOLID, + Function:fTraceFilterFunction=INVALID_FUNCTION, + any:iTraceFilterFunctionData=-1) +{ + // Forward direction vector. + decl Float:flOriginalMoveDir[3], Float:flLateralDir[3]; + SubtractVectors(flOriginalMovePos, flOriginalFeetPos, flOriginalMoveDir); + flOriginalMoveDir[2] = 0.0; + + GetVectorAngles(flOriginalMoveDir, flOriginalMoveDir); + GetAngleVectors(flOriginalMoveDir, flOriginalMoveDir, flLateralDir, NULL_VECTOR); + NormalizeVector(flOriginalMoveDir, flOriginalMoveDir); + NormalizeVector(flLateralDir, flLateralDir); + NegateVector(flLateralDir); + + // Correct move direction vector along floor. + decl Float:flDir[3]; + GetVectorCrossProduct(flLateralDir, flFloorNormalDir, flDir); + NormalizeVector(flDir, flDir); + + // Correct lateral direction vector along floor. + GetVectorCrossProduct(flDir, flFloorNormalDir, flLateralDir); + NormalizeVector(flLateralDir, flLateralDir); + + if (flFeelerHeight <= 0.0) + { + flFeelerHeight = StepHeight + 0.1; + } + + decl Float:flFeetPos[3]; + CopyVector(flOriginalFeetPos, flFeetPos); + flFeetPos[2] += flFeelerHeight; + + decl Float:flFromPos[3]; + decl Float:flToPos[3]; + + // Check the left. + for (new i = 0; i < 3; i++) + { + flFromPos[i] = flFeetPos[i] + (flFeelerOffset * flLateralDir[i]); + flToPos[i] = flFromPos[i] + (flFeelerLength * flDir[i]); + } + + new Handle:hTrace = INVALID_HANDLE; + if (fTraceFilterFunction != INVALID_FUNCTION) + { + hTrace = TR_TraceRayFilterEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint, fTraceFilterFunction, iTraceFilterFunctionData); + } + else + { + hTrace = TR_TraceRayEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint); + } + + new bool:bLeftClear = !TR_DidHit(hTrace); + CloseHandle(hTrace); + +#if defined DEBUG + + if (bLeftClear) + { + TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 0, 255, 0, 255 }, 30); + } + else + { + TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 255, 0, 0, 255 }, 30); + } + + TE_SendToAll(); + +#endif + + // Check the right. + for (new i = 0; i < 3; i++) + { + flFromPos[i] = flFeetPos[i] - (flFeelerOffset * flLateralDir[i]); + flToPos[i] = flFromPos[i] + (flFeelerLength * flDir[i]); + } + + if (fTraceFilterFunction != INVALID_FUNCTION) + { + hTrace = TR_TraceRayFilterEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint, fTraceFilterFunction, iTraceFilterFunctionData); + } + else + { + hTrace = TR_TraceRayEx(flFromPos, flToPos, iTraceMask, RayType_EndPoint); + } + + new bool:bRightClear = !TR_DidHit(hTrace); + CloseHandle(hTrace); + +#if defined DEBUG + + if (bRightClear) + { + TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 0, 255, 0, 255 }, 30); + } + else + { + TE_SetupBeamPoints(flFromPos, flToPos, PrecacheModel("sprites/laser.vmt"), PrecacheModel("sprites/laser.vmt"), 0, 30, 0.1, 5.0, 5.0, 1, 0.0, { 255, 0, 0, 255 }, 30); + } + + TE_SendToAll(); + +#endif + + if (!bRightClear) + { + if (bLeftClear) + { + for (new i = 0; i < 3; i++) + { + flBuffer[i] = flOriginalMovePos[i] + (flAvoidRange * flLateralDir[i]); + } + } + } + else if (!bLeftClear) + { + for (new i = 0; i < 3; i++) + { + flBuffer[i] = flOriginalMovePos[i] - (flAvoidRange * flLateralDir[i]); + } + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/npc.sp b/addons/sourcemod/scripting/rytp_horror/npc.sp index 06b0cde..e916e36 100644 --- a/addons/sourcemod/scripting/rytp_horror/npc.sp +++ b/addons/sourcemod/scripting/rytp_horror/npc.sp @@ -1,2679 +1,2675 @@ -#if defined _sf2_npc_included - #endinput -#endif -#define _sf2_npc_included - -#define SF2_BOSS_PAGE_CALCULATION 0.3 -#define SF2_BOSS_COPY_SPAWN_MIN_DISTANCE 1850.0 // The default minimum distance boss copies can spawn from each other. - -#define SF2_BOSS_ATTACK_MELEE 0 - -static g_iNPCGlobalUniqueID = 0; - -static g_iNPCUniqueID[MAX_BOSSES] = { -1, ... }; -static String:g_strSlenderProfile[MAX_BOSSES][SF2_MAX_PROFILE_NAME_LENGTH]; -static g_iNPCProfileIndex[MAX_BOSSES] = { -1, ... }; -static g_iNPCUniqueProfileIndex[MAX_BOSSES] = { -1, ... }; -static g_iNPCType[MAX_BOSSES] = { SF2BossType_Unknown, ... }; -static g_iNPCFlags[MAX_BOSSES] = { 0, ... }; -static Float:g_flNPCModelScale[MAX_BOSSES] = { 1.0, ... }; - -static Float:g_flNPCFieldOfView[MAX_BOSSES] = { 0.0, ... }; -static Float:g_flNPCTurnRate[MAX_BOSSES] = { 0.0, ... }; - -static g_iSlender[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; - -static Float:g_flNPCSpeed[MAX_BOSSES][Difficulty_Max]; -static Float:g_flNPCMaxSpeed[MAX_BOSSES][Difficulty_Max]; - -static Float:g_flNPCScareRadius[MAX_BOSSES]; -static Float:g_flNPCScareCooldown[MAX_BOSSES]; - -static g_iNPCTeleportType[MAX_BOSSES] = { -1, ... }; - -static Float:g_flNPCAnger[MAX_BOSSES] = { 1.0, ... }; -static Float:g_flNPCAngerAddOnPageGrab[MAX_BOSSES] = { 0.0, ... }; -static Float:g_flNPCAngerAddOnPageGrabTimeDiff[MAX_BOSSES] = { 0.0, ... }; - -static Float:g_flNPCSearchRadius[MAX_BOSSES] = { 0.0, ... }; -static Float:g_flNPCInstantKillRadius[MAX_BOSSES] = { 0.0, ... }; - -static bool:g_bNPCDeathCamEnabled[MAX_BOSSES] = { false, ... }; - -static g_iNPCEnemy[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; - -#if defined METHODMAPS - -const SF2NPC_BaseNPC SF2_INVALID_NPC = SF2NPC_BaseNPC:-1; - -methodmap SF2NPC_BaseNPC -{ - property int Index - { - public get() { return _:this; } - } - - property int Type - { - public get() { return NPCGetType(this.Index); } - } - - property int ProfileIndex - { - public get() { return NPCGetProfileIndex(this.Index); } - } - - property int UniqueProfileIndex - { - public get() { return NPCGetUniqueProfileIndex(this.Index); } - } - - property int EntRef - { - public get() { return NPCGetEntRef(this.Index); } - } - - property int EntIndex - { - public get() { return NPCGetEntIndex(this.Index); } - } - - property int Flags - { - public get() { return NPCGetFlags(this.Index); } - public set(int flags) { NPCSetFlags(this.Index); } - } - - property float ModelScale - { - public get() { return NPCGetModelScale(this.Index) }; - } - - property float TurnRate - { - public get() { return NPCGetTurnRate(this.Index) }; - } - - property float FOV - { - public get() { return NPCGetFOV(this.Index); } - } - - property float Anger - { - public get() { return NPCGetAnger(this.Index); } - public set(float amount) { NPCSetAnger(this.Index, amount); } - } - - property float AngerAddOnPageGrab - { - public get() { return NPCGetAngerAddOnPageGrab(this.Index); } - } - - property float AngerAddOnPageGrabTimeDiff - { - public get() { return NPCGetAngerAddOnPageGrabTimeDiff(this.Index); } - } - - property float SearchRadius - { - public get() { return NPCGetSearchRadius(this.Index); } - } - - property float ScareRadius - { - public get() { return NPCGetScareRadius(this.Index); } - } - - property float ScareCooldown - { - public get() { return NPCGetScareCooldown(this.Index); } - } - - property float InstantKillRadius - { - public get() { return NPCGetInstantKillRadius(this.Index); } - } - - property int TeleportType - { - public get() { return NPCGetTeleportType(this.Index); } - } - - property int Enemy - { - public get() { return NPCGetEnemy(this.Index); } - public set(int entIndex) { NPCSetEnemy(this.Index, entIndex); } - } - - property bool DeathCamEnabled - { - public get() { return NPCHasDeathCamEnabled(this.Index); } - public set(bool state) { NPCSetDeathCamEnabled(this.Index, state); } - } - - public SF2NPC_BaseNPC(int index) - { - return SF2NPC_BaseNPC:index; - } - - public ~SF2NPC_BaseNPC() - { - NPCRemove(this.Index); - } - - public bool IsValid() - { - return NPCIsValid(this.Index); - } - - public void GetProfile(char[] buffer, int bufferlen) - { - NPCGetProfile(this.Index, buffer, bufferlen); - } - - public void SetProfile(const char[] profileName) - { - NPCSetProfile(this.Index, profileName); - } - - public float GetSpeed(int difficulty) - { - return NPCGetSpeed(this.Index, difficulty); - } - - public float GetMaxSpeed(int difficulty) - { - return NPCGetMaxSpeed(this.Index, difficulty); - } - - public void GetEyePosition(float buffer[3], const float defaultValue[3] = { 0.0, 0.0, 0.0 }) - { - NPCGetEyePosition(this.Index, buffer, defaultValue); - } - - public void GetEyePositionOffset(float buffer[3]) - { - NPCGetEyePositionOffset(this.Index, buffer); - } - - public void AddAnger(float amount) - { - NPCAddAnger(this.Index, amount); - } - - public bool HasAttribute(const char[] attributeName) - { - return NPCHasAttribute(this.Index, attributeName); - } - - public float GetAttributeValue(const char[] attributeName, float defaultValue = 0.0) - { - return NPCGetAttributeValue(this.Index, attributeName, defaultValue); - } -} - -#endif - -bool:NPCHasDeathCamEnabled(iNPCIndex) -{ - return g_bNPCDeathCamEnabled[iNPCIndex]; -} - -NPCSetDeathCamEnabled(iNPCIndex, bool:state) -{ - g_bNPCDeathCamEnabled[iNPCIndex] = state; -} - -public NPCInitialize() -{ - NPCChaserInitialize(); -} - -public NPCOnConfigsExecuted() -{ - g_iNPCGlobalUniqueID = 0; -} - -bool:NPCIsValid(iNPCIndex) -{ - return bool:(iNPCIndex >= 0 && iNPCIndex < MAX_BOSSES && NPCGetUniqueID(iNPCIndex) != -1); -} - -NPCGetUniqueID(iNPCIndex) -{ - return g_iNPCUniqueID[iNPCIndex]; -} - -NPCGetFromUniqueID(iNPCUniqueID) -{ - if (iNPCUniqueID == -1) return -1; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == iNPCUniqueID) - { - return i; - } - } - - return -1; -} - -NPCGetEntRef(iNPCIndex) -{ - return g_iSlender[iNPCIndex]; -} - -NPCGetEntIndex(iNPCIndex) -{ - return EntRefToEntIndex(NPCGetEntRef(iNPCIndex)); -} - -NPCGetFromEntIndex(entity) -{ - if (!entity || !IsValidEntity(entity)) return -1; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetEntIndex(i) == entity) - { - return i; - } - } - - return -1; -} - -NPCGetCount() -{ - new iCount; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - if (NPCGetFlags(i) & SFF_FAKE) continue; - - iCount++; - } - - return iCount; -} - -NPCGetProfileIndex(iNPCIndex) -{ - return g_iNPCProfileIndex[iNPCIndex]; -} - -NPCGetUniqueProfileIndex(iNPCIndex) -{ - return g_iNPCUniqueProfileIndex[iNPCIndex]; -} - -bool:NPCGetProfile(iNPCIndex, String:buffer[], bufferlen) -{ - if(iNPCIndex < 0 || iNPCIndex >= sizeof(g_strSlenderProfile)) { - return false; - } - - strcopy(buffer, bufferlen, g_strSlenderProfile[iNPCIndex]); - return true; -} - -NPCSetProfile(iNPCIndex, const String:sProfile[]) -{ - strcopy(g_strSlenderProfile[iNPCIndex], sizeof(g_strSlenderProfile[]), sProfile); -} - -NPCRemove(iNPCIndex) -{ - if (!NPCIsValid(iNPCIndex)) return; - - RemoveProfile(iNPCIndex); -} - -NPCRemoveAll() -{ - for (new i = 0; i < MAX_BOSSES; i++) - { - NPCRemove(i); - } -} - -NPCGetType(iNPCIndex) -{ - return g_iNPCType[iNPCIndex]; -} - -NPCGetFlags(iNPCIndex) -{ - return g_iNPCFlags[iNPCIndex]; -} - -NPCSetFlags(iNPCIndex, iFlags) -{ - g_iNPCFlags[iNPCIndex] = iFlags; -} - -Float:NPCGetModelScale(iNPCIndex) -{ - return g_flNPCModelScale[iNPCIndex]; -} - -Float:NPCGetSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCSpeed[iNPCIndex][iDifficulty]; -} - -Float:NPCGetMaxSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCMaxSpeed[iNPCIndex][iDifficulty]; -} - -Float:NPCGetTurnRate(iNPCIndex) -{ - return g_flNPCTurnRate[iNPCIndex]; -} - -Float:NPCGetFOV(iNPCIndex) -{ - return g_flNPCFieldOfView[iNPCIndex]; -} - -Float:NPCGetAnger(iNPCIndex) -{ - return g_flNPCAnger[iNPCIndex]; -} - -NPCSetAnger(iNPCIndex, Float:flAnger) -{ - g_flNPCAnger[iNPCIndex] = flAnger; -} - -NPCAddAnger(iNPCIndex, Float:flAmount) -{ - g_flNPCAnger[iNPCIndex] += flAmount; -} - -Float:NPCGetAngerAddOnPageGrab(iNPCIndex) -{ - return g_flNPCAngerAddOnPageGrab[iNPCIndex]; -} - -Float:NPCGetAngerAddOnPageGrabTimeDiff(iNPCIndex) -{ - return g_flNPCAngerAddOnPageGrabTimeDiff[iNPCIndex]; -} - -NPCGetEyePositionOffset(iNPCIndex, Float:buffer[3]) -{ - buffer[0] = g_flSlenderEyePosOffset[iNPCIndex][0]; - buffer[1] = g_flSlenderEyePosOffset[iNPCIndex][1]; - buffer[2] = g_flSlenderEyePosOffset[iNPCIndex][2]; -} - -Float:NPCGetSearchRadius(iNPCIndex) -{ - return g_flNPCSearchRadius[iNPCIndex]; -} - -Float:NPCGetScareRadius(iNPCIndex) -{ - return g_flNPCScareRadius[iNPCIndex]; -} - -Float:NPCGetScareCooldown(iNPCIndex) -{ - return g_flNPCScareCooldown[iNPCIndex]; -} - -Float:NPCGetInstantKillRadius(iNPCIndex) -{ - return g_flNPCInstantKillRadius[iNPCIndex]; -} - -NPCGetTeleportType(iNPCIndex) -{ - return g_iNPCTeleportType[iNPCIndex]; -} - -stock NPCGetEnemy(iNPCIndex) -{ - return g_iNPCEnemy[iNPCIndex]; -} - -stock NPCSetEnemy(iNPCIndex, ent) -{ - g_iNPCEnemy[iNPCIndex] = IsValidEntity(ent) ? EntIndexToEntRef(ent) : INVALID_ENT_REFERENCE; -} - -/** - * Returns the boss's eye position (eye pos offset + absorigin). - */ -bool:NPCGetEyePosition(iNPCIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) -{ - buffer[0] = flDefaultValue[0]; - buffer[1] = flDefaultValue[1]; - buffer[2] = flDefaultValue[2]; - - if (!NPCIsValid(iNPCIndex)) return false; - - new iNPC = NPCGetEntIndex(iNPCIndex); - if (!iNPC || iNPC == INVALID_ENT_REFERENCE) return false; - - // @TODO: Replace SlenderGetAbsOrigin with GetEntPropVector - decl Float:flPos[3], Float:flEyePosOffset[3]; - SlenderGetAbsOrigin(iNPCIndex, flPos); - NPCGetEyePositionOffset(iNPCIndex, flEyePosOffset); - - AddVectors(flPos, flEyePosOffset, buffer); - return true; -} - -bool:NPCHasAttribute(iNPCIndex, const String:sAttribute[]) -{ - if (NPCGetUniqueID(iNPCIndex) == -1) return false; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iNPCIndex, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - if (!KvJumpToKey(g_hConfig, "attributes")) return false; - - return KvJumpToKey(g_hConfig, sAttribute); -} - -Float:NPCGetAttributeValue(iNPCIndex, const String:sAttribute[], Float:flDefaultValue=0.0) -{ - if (!NPCHasAttribute(iNPCIndex, sAttribute)) return flDefaultValue; - return KvGetFloat(g_hConfig, "value", flDefaultValue); -} - -bool:SlenderCanRemove(iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) return false; - - if (PeopleCanSeeSlender(iBossIndex, _, false)) return false; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iTeleportType = GetProfileNum(sProfile, "teleport_type"); - - switch (iTeleportType) - { - case 0: - { - if (GetProfileNum(sProfile, "static_on_radius")) - { - decl Float:flSlenderPos[3], Float:flBuffer[3]; - SlenderGetAbsOrigin(iBossIndex, flSlenderPos); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - IsClientInGhostMode(i) || - IsClientInDeathCam(i)) continue; - - if (!IsPointVisibleToPlayer(i, flSlenderPos, false, false)) continue; - - GetClientAbsOrigin(i, flBuffer); - if (GetVectorDistance(flBuffer, flSlenderPos) <= GetProfileFloat(sProfile, "static_radius")) - { - return false; - } - } - } - } - case 1: - { - if (PeopleCanSeeSlender(iBossIndex, _, SlenderUsesBlink(iBossIndex)) || PeopleCanSeeSlender(iBossIndex, false, false)) - { - return false; - } - } - case 2: - { - new iState = g_iSlenderState[iBossIndex]; - if (iState == STATE_IDLE || iState == STATE_WANDER) - { - if (GetGameTime() < g_flSlenderTimeUntilKill[iBossIndex]) - { - return false; - } - } - else - { - return false; - } - } - } - - return true; -} - -bool:SlenderGetAbsOrigin(iBossIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) -{ - for (new i = 0; i < 3; i++) buffer[i] = flDefaultValue[i]; - - if (iBossIndex < 0 || NPCGetUniqueID(iBossIndex) == -1) return false; - - new slender = NPCGetEntIndex(iBossIndex); - if (!slender || slender == INVALID_ENT_REFERENCE) return false; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl Float:flPos[3], Float:flOffset[3]; - GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flPos); - GetProfileVector(sProfile, "pos_offset", flOffset, flDefaultValue); - SubtractVectors(flPos, flOffset, buffer); - - return true; -} - -bool:SlenderGetEyePosition(iBossIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) -{ - return NPCGetEyePosition(iBossIndex, buffer, flDefaultValue); -} - -bool:SelectProfile(iBossIndex, const String:sProfile[], iAdditionalBossFlags=0, iCopyMaster=-1, bool:bSpawnCompanions=true, bool:bPlaySpawnSound=true) -{ - if (!IsProfileValid(sProfile)) - { - LogSF2Message("Could not select profile for boss %d: profile %s is invalid!", iBossIndex, sProfile); - return false; - } - - NPCRemove(iBossIndex); - - new iProfileIndex = GetBossProfileIndexFromName(sProfile); - new iUniqueProfileIndex = GetBossProfileUniqueProfileIndex(iProfileIndex); - - NPCSetProfile(iBossIndex, sProfile); - - new iBossType = GetBossProfileType(iProfileIndex); - - g_iNPCProfileIndex[iBossIndex] = iProfileIndex; - g_iNPCUniqueProfileIndex[iBossIndex] = iUniqueProfileIndex; - g_iNPCUniqueID[iBossIndex] = g_iNPCGlobalUniqueID++; - g_iNPCType[iBossIndex] = iBossType; - - g_flNPCModelScale[iBossIndex] = GetBossProfileModelScale(iProfileIndex); - - NPCSetFlags(iBossIndex, GetBossProfileFlags(iProfileIndex) | iAdditionalBossFlags); - - GetBossProfileEyePositionOffset(iProfileIndex, g_flSlenderEyePosOffset[iBossIndex]); - GetBossProfileEyeAngleOffset(iProfileIndex, g_flSlenderEyeAngOffset[iBossIndex]); - - GetProfileVector(sProfile, "mins", g_flSlenderDetectMins[iBossIndex]); - GetProfileVector(sProfile, "maxs", g_flSlenderDetectMaxs[iBossIndex]); - - NPCSetAnger(iBossIndex, GetBossProfileAngerStart(iProfileIndex)); - g_flNPCAngerAddOnPageGrab[iBossIndex] = GetBossProfileAngerAddOnPageGrab(iProfileIndex); - g_flNPCAngerAddOnPageGrabTimeDiff[iBossIndex] = GetBossProfileAngerPageGrabTimeDiff(iProfileIndex); - - g_iSlenderCopyMaster[iBossIndex] = -1; - g_iSlenderHealth[iBossIndex] = GetProfileNum(sProfile, "health", 900); - - for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) - { - g_flNPCSpeed[iBossIndex][iDifficulty] = GetBossProfileSpeed(iProfileIndex, iDifficulty); - g_flNPCMaxSpeed[iBossIndex][iDifficulty] = GetBossProfileMaxSpeed(iProfileIndex, iDifficulty); - } - - g_flNPCTurnRate[iBossIndex] = GetBossProfileTurnRate(iProfileIndex); - g_flNPCFieldOfView[iBossIndex] = GetBossProfileFOV(iProfileIndex); - - g_flNPCSearchRadius[iBossIndex] = GetBossProfileSearchRadius(iProfileIndex); - - g_flNPCScareRadius[iBossIndex] = GetBossProfileScareRadius(iProfileIndex); - g_flNPCScareCooldown[iBossIndex] = GetBossProfileScareCooldown(iProfileIndex); - - g_flNPCInstantKillRadius[iBossIndex] = GetBossProfileInstantKillRadius(iProfileIndex); - - g_iNPCTeleportType[iBossIndex] = GetBossProfileTeleportType(iProfileIndex); - - g_iNPCEnemy[iBossIndex] = INVALID_ENT_REFERENCE; - - // Deathcam values. - NPCSetDeathCamEnabled(iBossIndex, bool:GetProfileNum(sProfile, "death_cam")); - - g_flSlenderAcceleration[iBossIndex] = GetProfileFloat(sProfile, "acceleration", 150.0); - g_hSlenderFakeTimer[iBossIndex] = INVALID_HANDLE; - g_hSlenderEntityThink[iBossIndex] = INVALID_HANDLE; - g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; - g_flSlenderNextTeleportTime[iBossIndex] = GetGameTime(); - g_flSlenderLastKill[iBossIndex] = GetGameTime(); - g_flSlenderTimeUntilKill[iBossIndex] = -1.0; - g_flSlenderNextJumpScare[iBossIndex] = -1.0; - g_flSlenderTimeUntilNextProxy[iBossIndex] = -1.0; - g_flSlenderTeleportMinRange[iBossIndex] = GetProfileFloat(sProfile, "teleport_range_min", 325.0); - g_flSlenderTeleportMaxRange[iBossIndex] = GetProfileFloat(sProfile, "teleport_range_max", 1024.0); - g_flSlenderStaticRadius[iBossIndex] = GetProfileFloat(sProfile, "static_radius"); - g_flSlenderIdleAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_idle_playbackrate", 1.0); - g_flSlenderWalkAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_walk_playbackrate", 1.0); - g_flSlenderRunAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_run_playbackrate", 1.0); - g_flSlenderJumpSpeed[iBossIndex] = GetProfileFloat(sProfile, "jump_speed", 512.0); - g_flSlenderPathNodeTolerance[iBossIndex] = GetProfileFloat(sProfile, "search_node_dist_tolerance", 32.0); - g_flSlenderPathNodeLookAhead[iBossIndex] = GetProfileFloat(sProfile, "search_node_dist_lookahead", 512.0); - g_flSlenderProxyTeleportMinRange[iBossIndex] = GetProfileFloat(sProfile, "proxies_teleport_range_min"); - g_flSlenderProxyTeleportMaxRange[iBossIndex] = GetProfileFloat(sProfile, "proxies_teleport_range_max"); - - for (new i = 1; i <= MaxClients; i++) - { - g_flPlayerLastChaseBossEncounterTime[i][iBossIndex] = -1.0; - g_flSlenderTeleportPlayersRestTime[iBossIndex][i] = -1.0; - } - - g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; - g_flSlenderNextTeleportTime[iBossIndex] = -1.0; - g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; - - g_hSlenderThink[iBossIndex] = CreateTimer(0.1, Timer_SlenderTeleportThink, iBossIndex, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - SlenderRemoveTargetMemory(iBossIndex); - - switch (iBossType) - { - case SF2BossType_Chaser: - { - NPCChaserOnSelectProfile(iBossIndex); - - SlenderCreateTargetMemory(iBossIndex); - } - } - - if (iCopyMaster >= 0 && iCopyMaster < MAX_BOSSES && NPCGetUniqueID(iCopyMaster) != -1) - { - g_iSlenderCopyMaster[iBossIndex] = iCopyMaster; - g_flSlenderNextJumpScare[iBossIndex] = g_flSlenderNextJumpScare[iCopyMaster]; - - NPCSetAnger(iBossIndex, NPCGetAnger(iCopyMaster)); - } - else - { - if (bPlaySpawnSound) - { - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_spawn_all", sBuffer, sizeof(sBuffer)); - if (sBuffer[0]) EmitSoundToAll(sBuffer, _, SNDCHAN_STATIC, SNDLEVEL_HELICOPTER); - } - - if (bSpawnCompanions) - { - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - decl String:sCompProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - new Handle:hCompanions = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); - - if (KvJumpToKey(g_hConfig, "companions")) - { - decl String:sNum[32]; - - for (new i = 1;;i++) - { - IntToString(i, sNum, sizeof(sNum)); - KvGetString(g_hConfig, sNum, sCompProfile, sizeof(sCompProfile)); - if (!sCompProfile[0]) break; - - PushArrayString(hCompanions, sCompProfile); - } - } - - for (new i = 0, iSize = GetArraySize(hCompanions); i < iSize; i++) - { - GetArrayString(hCompanions, i, sCompProfile, sizeof(sCompProfile)); - AddProfile(sCompProfile, _, _, false, false); - } - - CloseHandle(hCompanions); - } - } - - Call_StartForward(fOnBossAdded); - Call_PushCell(iBossIndex); - Call_Finish(); - - return true; -} - -AddProfile(const String:strName[], iAdditionalBossFlags=0, iCopyMaster=-1, bool:bSpawnCompanions=true, bool:bPlaySpawnSound=true) -{ - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) - { - if (SelectProfile(i, strName, iAdditionalBossFlags, iCopyMaster, bSpawnCompanions, bPlaySpawnSound)) - { - return i; - } - - break; - } - } - - return -1; -} - -RemoveProfile(iBossIndex) -{ - RemoveSlender(iBossIndex); - - // Call our forward. - Call_StartForward(fOnBossRemoved); - Call_PushCell(iBossIndex); - Call_Finish(); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - NPCChaserOnRemoveProfile(iBossIndex); - - // Remove all possible sounds, for emergencies. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - // Remove chase music. - if (g_iPlayerChaseMusicMaster[i] == iBossIndex) - { - ClientStopAllSlenderSounds(i, sProfile, "sound_chase", SNDCHAN_AUTO); - } - } - - // Clean up on the clients. - for (new i = 1; i <= MaxClients; i++) - { - g_flSlenderLastFoundPlayer[iBossIndex][i] = -1.0; - g_flPlayerLastChaseBossEncounterTime[i][iBossIndex] = -1.0; - g_flSlenderTeleportPlayersRestTime[iBossIndex][i] = -1.0; - - for (new i2 = 0; i2 < 3; i2++) - { - g_flSlenderLastFoundPlayerPos[iBossIndex][i][i2] = 0.0; - } - - if (IsClientInGame(i)) - { - if (NPCGetUniqueID(iBossIndex) == g_iPlayerStaticMaster[i]) - { - g_iPlayerStaticMaster[i] = -1; - - // No one is the static master. - g_hPlayerStaticTimer[i] = CreateTimer(g_flPlayerStaticDecreaseRate[i], - Timer_ClientDecreaseStatic, - GetClientUserId(i), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerStaticTimer[i], true); - } - } - } - - g_iNPCTeleportType[iBossIndex] = -1; - g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; - g_flSlenderNextTeleportTime[iBossIndex] = -1.0; - g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; - g_flSlenderTimeUntilKill[iBossIndex] = -1.0; - - // Remove all copies associated with me. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (i == iBossIndex || NPCGetUniqueID(i) == -1) continue; - - if (g_iSlenderCopyMaster[i] == iBossIndex) - { - LogMessage("Removed boss index %d because it is a copy of boss index %d", i, iBossIndex); - NPCRemove(i); - } - } - - NPCSetProfile(iBossIndex, ""); - g_iNPCType[iBossIndex] = -1; - g_iNPCProfileIndex[iBossIndex] = -1; - g_iNPCUniqueProfileIndex[iBossIndex] = -1; - - NPCSetFlags(iBossIndex, 0); - - NPCSetAnger(iBossIndex, 1.0); - - g_flNPCFieldOfView[iBossIndex] = 0.0; - - g_iNPCEnemy[iBossIndex] = INVALID_ENT_REFERENCE; - - NPCSetDeathCamEnabled(iBossIndex, false); - - g_iSlenderCopyMaster[iBossIndex] = -1; - g_iNPCUniqueID[iBossIndex] = -1; - g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; - g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; - g_hSlenderThink[iBossIndex] = INVALID_HANDLE; - g_hSlenderEntityThink[iBossIndex] = INVALID_HANDLE; - - g_hSlenderFakeTimer[iBossIndex] = INVALID_HANDLE; - g_flSlenderLastKill[iBossIndex] = -1.0; - g_iSlenderState[iBossIndex] = STATE_IDLE; - g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_iSlenderModel[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderAcceleration[iBossIndex] = 0.0; - g_flSlenderTimeUntilNextProxy[iBossIndex] = -1.0; - g_flNPCSearchRadius[iBossIndex] = 0.0; - g_flNPCInstantKillRadius[iBossIndex] = 0.0; - g_flNPCScareRadius[iBossIndex] = 0.0; - g_flSlenderProxyTeleportMinRange[iBossIndex] = 0.0; - g_flSlenderProxyTeleportMaxRange[iBossIndex] = 0.0; - - for (new i = 0; i < 3; i++) - { - g_flSlenderDetectMins[iBossIndex][i] = 0.0; - g_flSlenderDetectMaxs[iBossIndex][i] = 0.0; - g_flSlenderEyePosOffset[iBossIndex][i] = 0.0; - } - - SlenderRemoveTargetMemory(iBossIndex); -} - -SpawnSlender(iBossIndex, const Float:pos[3]) -{ - RemoveSlender(iBossIndex); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl Float:flTruePos[3]; - GetProfileVector(sProfile, "pos_offset", flTruePos); - AddVectors(flTruePos, pos, flTruePos); - - new iSlenderModel = SpawnSlenderModel(iBossIndex, flTruePos); - if (iSlenderModel == -1) - { - LogError("Could not spawn boss: model failed to spawn!"); - return; - } - - decl String:sBuffer[PLATFORM_MAX_PATH]; - - g_iSlenderModel[iBossIndex] = EntIndexToEntRef(iSlenderModel); - - switch (NPCGetType(iBossIndex)) - { - case SF2BossType_Creeper: - { - g_iSlender[iBossIndex] = g_iSlenderModel[iBossIndex]; - g_hSlenderEntityThink[iBossIndex] = CreateTimer(BOSS_THINKRATE, Timer_SlenderBlinkBossThink, g_iSlender[iBossIndex], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - case SF2BossType_Chaser: - { - GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); - - new iBoss = CreateEntityByName("monster_generic"); - SetEntityModel(iBoss, sBuffer); - TeleportEntity(iBoss, flTruePos, NULL_VECTOR, NULL_VECTOR); - DispatchSpawn(iBoss); - ActivateEntity(iBoss); - SetEntityRenderMode(iBoss, RENDER_TRANSCOLOR); - SetEntityRenderColor(iBoss, 0, 0, 0, 1); - SetVariantString("!activator"); - AcceptEntityInput(iSlenderModel, "SetParent", iBoss); - AcceptEntityInput(iSlenderModel, "EnableShadow"); - SetEntProp(iSlenderModel, Prop_Send, "m_usSolidFlags", FSOLID_NOT_SOLID | FSOLID_TRIGGER); - AcceptEntityInput(iBoss, "DisableShadow"); - SetEntPropFloat(iBoss, Prop_Data, "m_flFriction", 0.0); - - NPCChaserSetStunHealth(iBossIndex, NPCChaserGetStunInitialHealth(iBossIndex)); - - // Reset stats. - g_iSlender[iBossIndex] = EntIndexToEntRef(iBoss); - g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_iSlenderState[iBossIndex] = STATE_IDLE; - g_bSlenderAttacking[iBossIndex] = false; - g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; - g_flSlenderTargetSoundLastTime[iBossIndex] = -1.0; - g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = -1.0; - g_iSlenderTargetSoundType[iBossIndex] = SoundType_None; - g_bSlenderInvestigatingSound[iBossIndex] = false; - g_flSlenderLastHeardFootstep[iBossIndex] = GetGameTime(); - g_flSlenderLastHeardVoice[iBossIndex] = GetGameTime(); - g_flSlenderLastHeardWeapon[iBossIndex] = GetGameTime(); - g_flSlenderNextVoiceSound[iBossIndex] = GetGameTime(); - g_flSlenderNextMoanSound[iBossIndex] = GetGameTime(); - g_flSlenderNextWanderPos[iBossIndex] = GetGameTime() + 3.0; - g_flSlenderTimeUntilKill[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "idle_lifetime", 10.0); - g_flSlenderTimeUntilRecover[iBossIndex] = -1.0; - g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; - g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; - g_flSlenderTimeUntilChase[iBossIndex] = -1.0; - g_flSlenderTimeUntilNoPersistence[iBossIndex] = -1.0; - g_flSlenderNextJump[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "jump_cooldown", 2.0); - g_flSlenderNextPathTime[iBossIndex] = GetGameTime(); - g_hSlenderEntityThink[iBossIndex] = CreateTimer(BOSS_THINKRATE, Timer_SlenderChaseBossThink, EntIndexToEntRef(iBoss), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_iSlenderInterruptConditions[iBossIndex] = 0; - g_bSlenderChaseDeathPosition[iBossIndex] = false; - - for (new i = 0; i < 3; i++) - { - g_flSlenderGoalPos[iBossIndex][i] = 0.0; - g_flSlenderTargetSoundTempPos[iBossIndex][i] = 0.0; - g_flSlenderTargetSoundMasterPos[iBossIndex][i] = 0.0; - g_flSlenderChaseDeathPosition[iBossIndex][i] = 0.0; - } - - for (new i = 1; i <= MaxClients; i++) - { - g_flSlenderLastFoundPlayer[iBossIndex][i] = -1.0; - - for (new i2 = 0; i2 < 3; i2++) - { - g_flSlenderLastFoundPlayerPos[iBossIndex][i][i2] = 0.0; - } - } - - SlenderClearTargetMemory(iBossIndex); - - if (GetProfileNum(sProfile, "stun_enabled")) - { - SetEntProp(iBoss, Prop_Data, "m_takedamage", 1); - } - - SDKHook(iBoss, SDKHook_OnTakeDamage, Hook_SlenderOnTakeDamage); - SDKHook(iBoss, SDKHook_OnTakeDamagePost, Hook_SlenderOnTakeDamagePost); - DHookEntity(g_hSDKShouldTransmit, true, iBoss); - } - /* - default: - { - g_iSlender[iBossIndex] = g_iSlenderModel[iBossIndex]; - SDKHook(iSlenderModel, SDKHook_SetTransmit, Hook_SlenderSetTransmit); - } - */ - } - - SDKHook(iSlenderModel, SDKHook_SetTransmit, Hook_SlenderModelSetTransmit); - - SlenderSpawnEffects(iBossIndex, EffectEvent_Constant); - - // Initialize our pose parameters, if needed. - new iPose = EntRefToEntIndex(g_iSlenderPoseEnt[iBossIndex]); - g_iSlenderPoseEnt[iBossIndex] = INVALID_ENT_REFERENCE; - if (iPose && iPose != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(iPose, "Kill"); - } - - decl String:sPoseParameter[64]; - GetProfileString(sProfile, "pose_parameter", sPoseParameter, sizeof(sPoseParameter)); - if (sPoseParameter[0]) - { - iPose = CreateEntityByName("point_posecontroller"); - if (iPose != -1) - { - // We got a pose parameter! We need a name! - Format(sBuffer, sizeof(sBuffer), "s%dposepls", g_iSlenderModel[iBossIndex]); - DispatchKeyValue(iSlenderModel, "targetname", sBuffer); - - DispatchKeyValue(iPose, "PropName", sBuffer); - DispatchKeyValue(iPose, "PoseParameterName", sPoseParameter); - DispatchKeyValueFloat(iPose, "PoseValue", GetProfileFloat(sProfile, "pose_parameter_max")); - DispatchSpawn(iPose); - SetVariantString(sPoseParameter); - AcceptEntityInput(iPose, "SetPoseParameterName"); - SetVariantString("!activator"); - AcceptEntityInput(iPose, "SetParent", iSlenderModel); - - g_iSlenderPoseEnt[iBossIndex] = EntIndexToEntRef(iPose); - } - } - - // Call our forward. - Call_StartForward(fOnBossSpawn); - Call_PushCell(iBossIndex); - Call_Finish(); -} - -RemoveSlender(iBossIndex) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iBoss = NPCGetEntIndex(iBossIndex); - g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; - - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - // Stop all possible looping sounds. - ClientStopAllSlenderSounds(iBoss, sProfile, "sound_move", SNDCHAN_AUTO); - - if (NPCGetFlags(iBossIndex) & SFF_HASSTATICLOOPLOCALSOUND) - { - decl String:sLoopSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); - - if (sLoopSound[0]) - { - StopSound(iBoss, SNDCHAN_STATIC, sLoopSound); - } - } - - AcceptEntityInput(iBoss, "Kill"); - } -} - -public Action:Hook_SlenderOnTakeDamage(slender, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return Plugin_Continue; - - if (NPCGetType(iBossIndex) == SF2BossType_Chaser) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (NPCChaserIsStunEnabled(iBossIndex)) - { - if (damagetype & DMG_ACID) damage *= 2.0; // 2x damage for critical hits. - - NPCChaserAddStunHealth(iBossIndex, -damage); - } - } - - damage = 0.0; - return Plugin_Changed; -} - -public Hook_SlenderOnTakeDamagePost(slender, attacker, inflictor, Float:damage, damagetype, weapon, const Float:damageForce[3], const Float:damagePosition[3]) -{ - if (!g_bEnabled) return; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return; - - if (NPCGetType(iBossIndex) == SF2BossType_Chaser) - { - if (damagetype & DMG_ACID) - { - decl Float:flMyEyePos[3]; - SlenderGetEyePosition(iBossIndex, flMyEyePos); - - TE_SetupTFParticleEffect(g_iParticleCriticalHit, flMyEyePos, flMyEyePos); - TE_SendToAll(); - - EmitSoundToAll(CRIT_SOUND, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - } - } -} - -public Action:Hook_SlenderModelSetTransmit(entity, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iBossIndex = -1; - - new entref = EntIndexToEntRef(entity); - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - if (g_iSlenderModel[i] != entref) continue; - - iBossIndex = i; - break; - } - - if (iBossIndex == -1) return Plugin_Continue; - - if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) return Plugin_Handled; - return Plugin_Continue; -} - -stock bool:SlenderCanHearPlayer(iBossIndex, client, SoundType:iSoundType) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client)) return false; - - new iSlender = NPCGetEntIndex(iBossIndex); - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) return false; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl Float:flHisPos[3], Float:flMyPos[3]; - GetClientAbsOrigin(client, flHisPos); - SlenderGetAbsOrigin(iBossIndex, flMyPos); - - new Float:flHearRadius = GetProfileFloat(sProfile, "search_sound_range", 1024.0); - if (flHearRadius <= 0.0) return false; - - new Float:flDistance = GetVectorDistance(flHisPos, flMyPos); - - // Trace check. - new Handle:hTrace = INVALID_HANDLE; - new bool:bTraceHit = false; - - decl Float:flMyEyePos[3]; - SlenderGetEyePosition(iBossIndex, flMyEyePos); - - if (iSoundType == SoundType_Footstep) - { - if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; - - if (GetEntProp(client, Prop_Send, "m_bDucking") || GetEntProp(client, Prop_Send, "m_bDucked")) flDistance *= 1.85; - if (IsClientReallySprinting(client)) flDistance *= 0.66; - - hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); - bTraceHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - } - else if (iSoundType == SoundType_Voice) - { - decl Float:flHisEyePos[3]; - GetClientEyePosition(client, flHisEyePos); - - hTrace = TR_TraceRayFilterEx(flMyEyePos, flHisEyePos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); - bTraceHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - flDistance *= 0.5; - } - else if (iSoundType == SoundType_Weapon) - { - decl Float:flHisMins[3], Float:flHisMaxs[3]; - GetEntPropVector(client, Prop_Send, "m_vecMins", flHisMins); - GetEntPropVector(client, Prop_Send, "m_vecMaxs", flHisMaxs); - - new Float:flMiddle[3]; - for (new i = 0; i < 2; i++) flMiddle[i] = (flHisMins[i] + flHisMaxs[i]) / 2.0; - - decl Float:flEndPos[3]; - GetClientAbsOrigin(client, flEndPos); - AddVectors(flHisPos, flMiddle, flEndPos); - - hTrace = TR_TraceRayFilterEx(flMyEyePos, flEndPos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); - bTraceHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - flDistance *= 0.66; - } - - if (bTraceHit) flDistance *= 1.66; - - if (TF2_GetPlayerClass(client) == TFClass_Spy) flDistance *= 1.35; - - if (flDistance > flHearRadius) return false; - - return true; -} - -stock SlenderArrayIndexToEntIndex(iBossIndex) -{ - return NPCGetEntIndex(iBossIndex); -} - -stock bool:SlenderOnlyLooksIfNotSeen(iBossIndex) -{ - if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return true; - return false; -} - -stock bool:SlenderUsesBlink(iBossIndex) -{ - if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return true; - return false; -} - -stock bool:SlenderKillsOnNear(iBossIndex) -{ - if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return false; - return true; -} - -stock SlenderClearTargetMemory(iBossIndex) -{ - if (iBossIndex == -1) return; - - g_iSlenderCurrentPathNode[iBossIndex] = -1; - if (g_hSlenderPath[iBossIndex] == INVALID_HANDLE) return; - - ClearArray(g_hSlenderPath[iBossIndex]); -} - -stock bool:SlenderCreateTargetMemory(iBossIndex) -{ - if (iBossIndex == -1) return false; - - g_iSlenderCurrentPathNode[iBossIndex] = -1; - if (g_hSlenderPath[iBossIndex] != INVALID_HANDLE) return true; - - g_hSlenderPath[iBossIndex] = CreateArray(3); - return true; -} - -stock SlenderRemoveTargetMemory(iBossIndex) -{ - if (iBossIndex == -1) return; - - g_iSlenderCurrentPathNode[iBossIndex] = -1; - - if (g_hSlenderPath[iBossIndex] == INVALID_HANDLE) return; - - new Handle:hLocs = g_hSlenderPath[iBossIndex]; - g_hSlenderPath[iBossIndex] = INVALID_HANDLE; - CloseHandle(hLocs); -} - -SlenderPerformVoice(iBossIndex, const String:sSectionName[], iIndex=-1) -{ - if (iBossIndex == -1) return; - - new slender = NPCGetEntIndex(iBossIndex); - if (!slender || slender == INVALID_ENT_REFERENCE) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sPath[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, sSectionName, sPath, sizeof(sPath), iIndex); - if (sPath[0]) - { - decl String:sBuffer[512]; - strcopy(sBuffer, sizeof(sBuffer), sSectionName); - StrCat(sBuffer, sizeof(sBuffer), "_cooldown_min"); - new Float:flCooldownMin = GetProfileFloat(sProfile, sBuffer, 1.5); - strcopy(sBuffer, sizeof(sBuffer), sSectionName); - StrCat(sBuffer, sizeof(sBuffer), "_cooldown_max"); - new Float:flCooldownMax = GetProfileFloat(sProfile, sBuffer, 1.5); - new Float:flCooldown = GetRandomFloat(flCooldownMin, flCooldownMax); - strcopy(sBuffer, sizeof(sBuffer), sSectionName); - StrCat(sBuffer, sizeof(sBuffer), "_volume"); - new Float:flVolume = GetProfileFloat(sProfile, sBuffer, 1.0); - strcopy(sBuffer, sizeof(sBuffer), sSectionName); - StrCat(sBuffer, sizeof(sBuffer), "_channel"); - new iChannel = GetProfileNum(sProfile, sBuffer, SNDCHAN_AUTO); - strcopy(sBuffer, sizeof(sBuffer), sSectionName); - StrCat(sBuffer, sizeof(sBuffer), "_level"); - new iLevel = GetProfileNum(sProfile, sBuffer, SNDLEVEL_SCREAMING); - - g_flSlenderNextVoiceSound[iBossIndex] = GetGameTime() + flCooldown; - EmitSoundToAll(sPath, slender, iChannel, iLevel, _, flVolume); - } -} - -bool:SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, Float:buffer[3]) -{ - if (!IsValidClient(iBestPlayer)) return false; - - new slender = NPCGetEntIndex(iBossIndex); - if (!slender || slender == INVALID_ENT_REFERENCE) return false; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl Float:flSlenderPos[3], Float:flPos[3], Float:flReferenceAng[3], Float:hisEyeAng[3], Float:tempDir[3], Float:tempPos[3]; - GetClientEyePosition(iBestPlayer, flPos); - - GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", hisEyeAng); - AddVectors(hisEyeAng, g_flSlenderEyeAngOffset[iBossIndex], hisEyeAng); - for (new i = 0; i < 3; i++) hisEyeAng[i] = AngleNormalize(hisEyeAng[i]); - - SlenderGetAbsOrigin(iBossIndex, flSlenderPos); - - SubtractVectors(flPos, flSlenderPos, flReferenceAng); - GetVectorAngles(flReferenceAng, flReferenceAng); - for (new i = 0; i < 3; i++) flReferenceAng[i] = AngleNormalize(flReferenceAng[i]); - new Float:flDist = GetProfileFloat(sProfile, "speed") * g_flRoundDifficultyModifier; - if (flDist < GetProfileFloat(sProfile, "kill_radius")) flDist = GetProfileFloat(sProfile, "kill_radius") / 2.0; - new Float:flWithinFOV = 45.0; - new Float:flWithinFOVSide = 90.0; - - decl Handle:hTrace, index, Float:flHitNormal[3], Float:tempPos2[3], Float:flBuffer[3], Float:flBuffer2[3]; - new Handle:hArray = CreateArray(6); - - decl Float:flCheckAng[3]; - - new iRange = 0; - new iID = 1; - - for (new Float:addAng = 0.0; addAng < 360.0; addAng += 7.5) - { - tempDir[0] = 0.0; - tempDir[1] = AngleNormalize(hisEyeAng[1] + addAng); - tempDir[2] = 0.0; - - GetAngleVectors(tempDir, tempDir, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(tempDir, tempDir); - ScaleVector(tempDir, flDist); - AddVectors(tempDir, flSlenderPos, tempPos); - AddVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); - AddVectors(flSlenderPos, g_flSlenderEyePosOffset[iBossIndex], tempPos2); - - flBuffer[0] = g_flSlenderDetectMins[iBossIndex][0]; - flBuffer[1] = g_flSlenderDetectMins[iBossIndex][1]; - flBuffer[2] = 0.0; - flBuffer2[0] = g_flSlenderDetectMaxs[iBossIndex][0]; - flBuffer2[1] = g_flSlenderDetectMaxs[iBossIndex][1]; - flBuffer2[2] = 0.0; - - // Get a good move position. - hTrace = TR_TraceHullFilterEx(tempPos2, tempPos, flBuffer, flBuffer2, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitCharactersOrEntity, slender); - TR_GetEndPosition(tempPos, hTrace); - CloseHandle(hTrace); - - // Drop to the ground if we're above ground. - hTrace = TR_TraceRayFilterEx(tempPos, Float:{ 90.0, 0.0, 0.0 }, MASK_PLAYERSOLID_BRUSHONLY, RayType_Infinite, TraceRayDontHitCharactersOrEntity, slender); - new bool:bHit = TR_DidHit(hTrace); - TR_GetEndPosition(tempPos2, hTrace); - CloseHandle(hTrace); - - // Then calculate from there. - hTrace = TR_TraceHullFilterEx(tempPos, tempPos2, g_flSlenderDetectMins[iBossIndex], g_flSlenderDetectMaxs[iBossIndex], MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitCharactersOrEntity, slender); - TR_GetEndPosition(tempPos, hTrace); - TR_GetPlaneNormal(hTrace, flHitNormal); - CloseHandle(hTrace); - SubtractVectors(tempPos, flSlenderPos, flCheckAng); - GetVectorAngles(flCheckAng, flCheckAng); - GetVectorAngles(flHitNormal, flHitNormal); - for (new i = 0; i < 3; i++) - { - flHitNormal[i] = AngleNormalize(flHitNormal[i]); - flCheckAng[i] = AngleNormalize(flCheckAng[i]); - } - - new Float:diff = AngleDiff(flCheckAng[1], flReferenceAng[1]); - - new bool:bBackup = false; - - if (FloatAbs(diff) > flWithinFOV) bBackup = true; - - if (diff >= 0.0 && diff <= flWithinFOVSide) iRange = 1; - else if (diff < 0.0 && diff >= -flWithinFOVSide) iRange = 2; - else continue; - - if ((flHitNormal[0] >= 0.0 && flHitNormal[0] < 45.0) - || (flHitNormal[0] < 0.0 && flHitNormal[0] > -45.0) - || !bHit - || TR_PointOutsideWorld(tempPos) - || IsSpaceOccupiedNPC(tempPos, g_flSlenderDetectMins[iBossIndex], g_flSlenderDetectMaxs[iBossIndex], iBestPlayer)) - { - continue; - } - - // Check from top to bottom of me. - - if (!IsPointVisibleToPlayer(iBestPlayer, tempPos, false, false)) continue; - - AddVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); - - if (!IsPointVisibleToPlayer(iBestPlayer, tempPos, false, false)) continue; - - SubtractVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); - - // Insert the vector into our array. - index = PushArrayCell(hArray, iID); - SetArrayCell(hArray, index, tempPos[0], 1); - SetArrayCell(hArray, index, tempPos[1], 2); - SetArrayCell(hArray, index, tempPos[2], 3); - SetArrayCell(hArray, index, iRange, 4); - SetArrayCell(hArray, index, bBackup, 5); - - iID++; - } - - new size; - if ((size = GetArraySize(hArray)) > 0) - { - new Float:diff = AngleDiff(hisEyeAng[1], flReferenceAng[1]); - if (diff >= 0.0) iRange = 1; - else iRange = 2; - - new bool:bBackup = false; - - // Clean up any vectors that we don't need. - new Handle:hArray2 = CloneArray(hArray); - for (new i = 0; i < size; i++) - { - if (GetArrayCell(hArray2, i, 4) != iRange || bool:GetArrayCell(hArray2, i, 5) != bBackup) - { - new iIndex = FindValueInArray(hArray, GetArrayCell(hArray2, i)); - if (iIndex != -1) RemoveFromArray(hArray, iIndex); - } - } - - CloseHandle(hArray2); - - size = GetArraySize(hArray); - if (size) - { - index = GetRandomInt(0, size - 1); - buffer[0] = Float:GetArrayCell(hArray, index, 1); - buffer[1] = Float:GetArrayCell(hArray, index, 2); - buffer[2] = Float:GetArrayCell(hArray, index, 3); - } - else - { - CloseHandle(hArray); - return false; - } - } - else - { - CloseHandle(hArray); - return false; - } - - CloseHandle(hArray); - return true; -} - -// This functor ensures that the proposed boss position is not too -// close to other players that are within the distance defined by -// flMinSearchDist. - -// Returning false on the functor will immediately discard the proposed position. - -public bool:SlenderChaseBossPlaceFunctor(iBossIndex, const Float:flActiveAreaCenterPos[3], const Float:flAreaPos[3], Float:flMinSearchDist, Float:flMaxSearchDist, bool:bOriginalResult) -{ - if (FloatAbs(flActiveAreaCenterPos[2] - flAreaPos[2]) > 320.0) - { - return false; - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - g_bPlayerEscaped[i]) continue; - - decl Float:flClientPos[3]; - GetClientAbsOrigin(i, flClientPos); - - if (GetVectorDistance(flClientPos, flAreaPos) < flMinSearchDist) - { - return false; - } - } - - return bOriginalResult; -} - -// As time passes on, we have to get more aggressive in order to successfully peak the target's -// stress level in the allotted duration we're given. Otherwise we'll be forced to place him -// in a rest period. - -// Teleport progressively closer as time passes in attempt to increase the target's stress level. -// Maximum minimum range is capped by the boss's anger level. - -stock Float:CalculateTeleportMinRange(iBossIndex, Float:flInitialMinRange, Float:flTeleportMaxRange) -{ - new Float:flTeleportTargetTimeLeft = g_flSlenderTeleportMaxTargetTime[iBossIndex] - GetGameTime(); - new Float:flTeleportTargetTimeInitial = g_flSlenderTeleportMaxTargetTime[iBossIndex] - g_flSlenderTeleportTargetTime[iBossIndex]; - new Float:flTeleportMinRange = flTeleportMaxRange - (1.0 - (flTeleportTargetTimeLeft / flTeleportTargetTimeInitial)) * (flTeleportMaxRange - flInitialMinRange); - - if (NPCGetAnger(iBossIndex) <= 1.0) - { - flTeleportMinRange += (g_flSlenderTeleportMinRange[iBossIndex] - flTeleportMaxRange) * Pow(NPCGetAnger(iBossIndex) - 1.0, 2.0 / g_flRoundDifficultyModifier); - } - - if (flTeleportMinRange < flInitialMinRange) flTeleportMinRange = flInitialMinRange; - if (flTeleportMinRange > flTeleportMaxRange) flTeleportMinRange = flTeleportMaxRange; - - return flTeleportMinRange; -} - -public Action:Timer_SlenderTeleportThink(Handle:timer, any:iBossIndex) -{ - if (iBossIndex == -1) return Plugin_Stop; - if (timer != g_hSlenderThink[iBossIndex]) return Plugin_Stop; - - if (NPCGetFlags(iBossIndex) & SFF_NOTELEPORT) return Plugin_Continue; - - // Check to see if anyone's looking at me before doing anything. - if (PeopleCanSeeSlender(iBossIndex, _, false)) - { - return Plugin_Continue; - } - - if (NPCGetTeleportType(iBossIndex) == 2) - { - new iBoss = NPCGetEntIndex(iBossIndex); - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - if (NPCGetType(iBossIndex) == SF2BossType_Chaser) - { - // Check to see if it's a good time to teleport away. - new iState = g_iSlenderState[iBossIndex]; - if (iState == STATE_IDLE || iState == STATE_WANDER) - { - if (GetGameTime() < g_flSlenderTimeUntilKill[iBossIndex]) - { - return Plugin_Continue; - } - } - } - } - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (!g_bRoundGrace) - { - if (GetGameTime() >= g_flSlenderNextTeleportTime[iBossIndex]) - { - new Float:flTeleportTime = GetRandomFloat(GetProfileFloat(sProfile, "teleport_time_min", 5.0), GetProfileFloat(sProfile, "teleport_time_max", 9.0)); - g_flSlenderNextTeleportTime[iBossIndex] = GetGameTime() + flTeleportTime; - - new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); - - if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) - { - // We don't have any good targets. Remove myself for now. - if (SlenderCanRemove(iBossIndex)) RemoveSlender(iBossIndex); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: no good target, removing...", iBossIndex); -#endif - } - else - { - new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderTeleportMinRange[iBossIndex], g_flSlenderTeleportMaxRange[iBossIndex]); - - new iTeleportAreaIndex = -1; - decl Float:flTeleportPos[3]; - - // Search surrounding nav areas around target. - if (NavMesh_Exists()) - { - decl Float:flTargetPos[3]; - GetClientAbsOrigin(iTeleportTarget, flTargetPos); - - new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); - if (iTargetAreaIndex != -1) - { - new bool:bShouldBeBehindObstruction = false; - if (NPCGetTeleportType(iBossIndex) == 2) - { - bShouldBeBehindObstruction = true; - } - - // Search outwards until travel distance is at maximum range. - new Handle:hAreaArray = CreateArray(2); - new Handle:hAreas = CreateStack(); - NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderTeleportMaxRange[iBossIndex]); - - { - new iPoppedAreas; - - while (!IsStackEmpty(hAreas)) - { - new iAreaIndex = -1; - PopStackCell(hAreas, iAreaIndex); - - // Check flags. - if (NavMeshArea_GetFlags(iAreaIndex) & NAV_MESH_NO_HOSTAGES) - { - // Don't spawn/teleport at areas marked with the "NO HOSTAGES" flag. - continue; - } - - new iIndex = PushArrayCell(hAreaArray, iAreaIndex); - SetArrayCell(hAreaArray, iIndex, float(NavMeshArea_GetCostSoFar(iAreaIndex)), 1); - iPoppedAreas++; - } - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: collected %d areas", iBossIndex, iPoppedAreas); -#endif - - CloseHandle(hAreas); - } - - new Handle:hAreaArrayClose = CreateArray(4); - new Handle:hAreaArrayAverage = CreateArray(4); - new Handle:hAreaArrayFar = CreateArray(4); - - for (new i = 1; i <= 3; i++) - { - new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(i - 1) / 3.0); - new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(i) / 3.0); - - for (new i2 = 0, iSize = GetArraySize(hAreaArray); i2 < iSize; i2++) - { - new iAreaIndex = GetArrayCell(hAreaArray, i2); - - decl Float:flAreaSpawnPoint[3]; - NavMeshArea_GetCenter(iAreaIndex, flAreaSpawnPoint); - - new iBoss = NPCGetEntIndex(iBossIndex); - - // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. - { - decl Float:flTraceStartPos[3]; - flTraceStartPos[0] = flAreaSpawnPoint[0]; - flTraceStartPos[1] = flAreaSpawnPoint[1]; - flTraceStartPos[2] = flAreaSpawnPoint[2] + (HalfHumanHeight * 2.0); - - decl Float:flTraceMins[3]; - flTraceMins[0] = g_flSlenderDetectMins[iBossIndex][0]; - flTraceMins[1] = g_flSlenderDetectMins[iBossIndex][1]; - flTraceMins[2] = 0.0; - - - decl Float:flTraceMaxs[3]; - flTraceMaxs[0] = g_flSlenderDetectMaxs[iBossIndex][0]; - flTraceMaxs[1] = g_flSlenderDetectMaxs[iBossIndex][1]; - flTraceMaxs[2] = 0.0; - - new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flAreaSpawnPoint, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - decl Float:flTraceHitPos[3]; - TR_GetEndPosition(flTraceHitPos, hTrace); - flTraceHitPos[2] += 1.0; - CloseHandle(hTrace); - - if (IsSpaceOccupiedNPC(flTraceHitPos, - g_flSlenderDetectMins[iBossIndex], - g_flSlenderDetectMaxs[iBossIndex], - iBoss)) - { - continue; - } - - if (NPCGetType(iBossIndex) == SF2BossType_Chaser) - { - if (IsSpaceOccupiedNPC(flTraceHitPos, - HULL_HUMAN_MINS, - HULL_HUMAN_MAXS, - iBoss)) - { - // Can't let an NPC spawn here; too little space. If we let it spawn here it will be non solid! - continue; - } - } - - flAreaSpawnPoint[0] = flTraceHitPos[0]; - flAreaSpawnPoint[1] = flTraceHitPos[1]; - flAreaSpawnPoint[2] = flTraceHitPos[2]; - } - - // Check visibility. - if (IsPointVisibleToAPlayer(flAreaSpawnPoint, !bShouldBeBehindObstruction, false)) continue; - - AddVectors(flAreaSpawnPoint, g_flSlenderEyePosOffset[iBossIndex], flAreaSpawnPoint); - - if (IsPointVisibleToAPlayer(flAreaSpawnPoint, !bShouldBeBehindObstruction, false)) continue; - - SubtractVectors(flAreaSpawnPoint, g_flSlenderEyePosOffset[iBossIndex], flAreaSpawnPoint); - - new bool:bTooNear = false; - - // Check minimum range with players. - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || - !IsPlayerAlive(iClient) || - g_bPlayerEliminated[iClient] || - IsClientInGhostMode(iClient) || - DidClientEscape(iClient)) - { - continue; - } - - decl Float:flTempPos[3]; - GetClientAbsOrigin(iClient, flTempPos); - - if (GetVectorDistance(flAreaSpawnPoint, flTempPos) <= g_flSlenderTeleportMinRange[iBossIndex]) - { - bTooNear = true; - break; - } - } - - if (bTooNear) continue; // This area is not compatible. - - // Check minimum range with boss copies (if supported). - if (NPCGetFlags(iBossIndex) & SFF_COPIES) - { - new Float:flMinDistBetweenBosses = GetProfileFloat(sProfile, "copy_teleport_dist_from_others", 800.0); - - for (new iBossCheck = 0; iBossCheck < MAX_BOSSES; iBossCheck++) - { - if (iBossCheck == iBossIndex || - NPCGetUniqueID(iBossCheck) == -1 || - (g_iSlenderCopyMaster[iBossIndex] != iBossCheck && g_iSlenderCopyMaster[iBossIndex] != g_iSlenderCopyMaster[iBossCheck])) - { - continue; - } - - new iBossEnt = NPCGetEntIndex(iBossCheck); - if (!iBossEnt || iBossEnt == INVALID_ENT_REFERENCE) continue; - - decl Float:flTempPos[3]; - SlenderGetAbsOrigin(iBossCheck, flTempPos); - - if (GetVectorDistance(flAreaSpawnPoint, flTempPos) <= flMinDistBetweenBosses) - { - bTooNear = true; - break; - } - } - } - - if (bTooNear) continue; // This area is not compatible. - - // Check travel distance and put in the appropriate arrays. - new Float:flDist = Float:GetArrayCell(hAreaArray, i2, 1); - if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) - { - new iIndex = -1; - new Handle:hTargetAreaArray = INVALID_HANDLE; - - switch (i) - { - case 1: - { - iIndex = PushArrayCell(hAreaArrayClose, iAreaIndex); - hTargetAreaArray = hAreaArrayClose; - } - case 2: - { - iIndex = PushArrayCell(hAreaArrayAverage, iAreaIndex); - hTargetAreaArray = hAreaArrayAverage; - } - case 3: - { - iIndex = PushArrayCell(hAreaArrayFar, iAreaIndex); - hTargetAreaArray = hAreaArrayFar; - } - } - - if (hTargetAreaArray != INVALID_HANDLE && iIndex != -1) - { - SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[0], 1); - SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[1], 2); - SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[2], 3); - } - } - } - } - - CloseHandle(hAreaArray); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: collected %d close areas, %d average areas, %d far areas", iBossIndex, GetArraySize(hAreaArrayClose), - GetArraySize(hAreaArrayAverage), - GetArraySize(hAreaArrayFar)); -#endif - - new iArrayIndex = -1; - - if (GetArraySize(hAreaArrayClose)) - { - iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1); - iTeleportAreaIndex = GetArrayCell(hAreaArrayClose, iArrayIndex); - flTeleportPos[0] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 1); - flTeleportPos[1] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 2); - flTeleportPos[2] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 3); - } - else if (GetArraySize(hAreaArrayAverage)) - { - iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1); - iTeleportAreaIndex = GetArrayCell(hAreaArrayAverage, iArrayIndex); - flTeleportPos[0] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 1); - flTeleportPos[1] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 2); - flTeleportPos[2] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 3); - } - else if (GetArraySize(hAreaArrayFar)) - { - iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1); - iTeleportAreaIndex = GetArrayCell(hAreaArrayFar, iArrayIndex); - flTeleportPos[0] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 1); - flTeleportPos[1] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 2); - flTeleportPos[2] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 3); - } - - CloseHandle(hAreaArrayClose); - CloseHandle(hAreaArrayAverage); - CloseHandle(hAreaArrayFar); - } - else - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because target is not on nav mesh!", iBossIndex); -#endif - } - } - else - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because of lack of nav mesh!", iBossIndex); -#endif - } - - if (iTeleportAreaIndex == -1) - { - // We don't have any good areas. Remove myself for now. - if (SlenderCanRemove(iBossIndex)) RemoveSlender(iBossIndex); - } - else - { - SpawnSlender(iBossIndex, flTeleportPos); - - if (NPCGetFlags(iBossIndex) & SFF_HASJUMPSCARE) - { - new bool:bDidJumpScare = false; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; - - if (PlayerCanSeeSlender(i, iBossIndex, false)) - { - if ((NPCGetDistanceFromEntity(iBossIndex, i) <= GetProfileFloat(sProfile, "jumpscare_distance") && - GetGameTime() >= g_flSlenderNextJumpScare[iBossIndex]) || - PlayerCanSeeSlender(i, iBossIndex)) - { - bDidJumpScare = true; - - new Float:flJumpScareDuration = GetProfileFloat(sProfile, "jumpscare_duration"); - ClientDoJumpScare(i, iBossIndex, flJumpScareDuration); - } - } - } - - if (bDidJumpScare) - { - g_flSlenderNextJumpScare[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "jumpscare_cooldown"); - } - } - } - } - } - else - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because of teleport time (curtime: %f, teletime: %f)", iBossIndex, GetGameTime(), g_flSlenderNextTeleportTime[iBossIndex]); -#endif - } - } - - return Plugin_Continue; -} - -/* -// Deprecated. - -// This is just to calculate the new place, not do time checks. -// Distance will be determined by the progression of the game and the -// manually set values determined by flMinSearchDist and flMaxSearchDist, -// which are float values that are (or should be) defined in the boss's -// config file. - -// The place chosen should be out of (possible) sight of the players, -// but should be within the AAS radius, the center being flActiveAreaCenterPos. -// The game will try to find a place that is of flMinSearchDist first, but -// if it can't, then it will try to find places that are a bit farther. - -// If the whole function fails, no place is given and the boss will not -// be able to spawn. - -bool:SlenderChaseBossCalculateNewPlace(iBossIndex, const Float:flActiveAreaCenterPos[3], Float:flMinSearchDist, Float:flMaxSearchDist, Function:iFunctor, Float:flBuffer[3]) -{ - new Handle:hAreas = NavMesh_GetAreas(); - if (hAreas == INVALID_HANDLE) return false; - - new iBestAreaIndex = -1; - new Float:flBestAreaDist = -1.0; - - decl Float:flAreaCenterPos[3]; - for (new i = 0, iSize = GetArraySize(hAreas); i < iSize; i++) - { - NavMeshArea_GetCenter(i, flAreaCenterPos); - - new Float:flDist = GetVectorDistance(flActiveAreaCenterPos, flAreaCenterPos); - if (flDist < flMinSearchDist || flDist > flMaxSearchDist) continue; - - if (IsPointVisibleToAPlayer(flAreaCenterPos, false, false)) continue; - - decl Float:flTestPos[3]; - for (new i2 = 0; i2 < 3; i2++) flTestPos[i2] = flAreaCenterPos[i2] + g_flSlenderEyePosOffset[iBossIndex][i2]; - - if (IsPointVisibleToAPlayer(flTestPos, false, false)) continue; - - if (iFunctor != INVALID_FUNCTION) - { - new bool:bResult = true; - - Call_StartFunction(INVALID_HANDLE, iFunctor); - Call_PushCell(iBossIndex); - Call_PushArray(flActiveAreaCenterPos, 3); - Call_PushArray(flAreaCenterPos, 3); - Call_PushFloat(flMinSearchDist); - Call_PushFloat(flMaxSearchDist); - Call_PushCell(bResult); - Call_Finish(bResult); - - if (!bResult) continue; - } - - if (flBestAreaDist < 0.0 || flDist < flBestAreaDist) - { - iBestAreaIndex = i; - flBestAreaDist = flDist; - } - } - - if (iBestAreaIndex == -1) return false; - - NavMeshArea_GetCenter(iBestAreaIndex, flBuffer); - return true; -} -*/ - -bool:SlenderCalculateNewPlace(iBossIndex, Float:buffer[3], bool:bIgnoreCopies=false, bool:bProxy=false, iProxyPlayer=-1, &iBestPlayer=-1) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new Float:flPercent = 0.0; - if (g_iPageMax > 0) - { - flPercent = (float(g_iPageCount) / float(g_iPageMax)) * g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex); - } - -#if defined DEBUG - new iArraySize, iArraySize2; -#endif - - if (!IsValidClient(iBestPlayer)) - { - // Pick a player to appear to. - new Handle:hArray = CreateArray(); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || - !IsPlayerAlive(i) || - IsClientInDeathCam(i) || - g_bPlayerEliminated[i] || - g_bPlayerEscaped[i]) continue; - - if (NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]) != -1 && !bIgnoreCopies) - { - new bool:bwub = false; - - // No? Then check if players around him are targeted by a boss already (not me). - for (new iBossPlayer = 1; iBossPlayer <= MaxClients; iBossPlayer++) - { - if (i == iBossPlayer) continue; - - if (!IsClientInGame(iBossPlayer) || - !IsPlayerAlive(iBossPlayer) || - IsClientInDeathCam(iBossPlayer) || - g_bPlayerEliminated[iBossPlayer] || - g_bPlayerEscaped[iBossPlayer]) continue; - - // Get the boss that's targeting this player, if any. - for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) - { - if (iBossIndex == iBoss || NPCGetUniqueID(iBoss) == -1) continue; - - if (EntRefToEntIndex(g_iSlenderTarget[iBoss]) == iBossPlayer) - { - // Are we near this player? - if (EntityDistanceFromEntity(iBossPlayer, i) < SF2_BOSS_COPY_SPAWN_MIN_DISTANCE) - { - bwub = true; - break; - } - } - } - } - - if (bwub) continue; - } - - PushArrayCell(hArray, i); - } - -#if defined DEBUG - iArraySize = GetArraySize(hArray); - iArraySize2 = iArraySize; -#endif - - if (GetArraySize(hArray)) - { - if (g_iSlenderCopyMaster[iBossIndex] == -1 || - GetProfileNum(sProfile, "copy_calculatepagecount", 0)) - { - new tempBestPageCount = -1; - - new Handle:hTempArray = CloneArray(hArray); - for (new i = 0; i < GetArraySize(hTempArray); i++) - { - new iClient = GetArrayCell(hTempArray, i); - if (g_iPlayerPageCount[iClient] > tempBestPageCount) - { - tempBestPageCount = g_iPlayerPageCount[iClient]; - } - } - - for (new i = 0; i < GetArraySize(hTempArray); i++) - { - new iClient = GetArrayCell(hTempArray, i); - if ((float(g_iPlayerPageCount[iClient]) / float(tempBestPageCount)) < SF2_BOSS_PAGE_CALCULATION) - { - new index = FindValueInArray(hArray, iClient); - if (index != -1) RemoveFromArray(hArray, index); - } - } - - CloseHandle(hTempArray); - } - -#if defined DEBUG - iArraySize2 = GetArraySize(hArray); -#endif - } - - if (GetArraySize(hArray)) - { - iBestPlayer = GetArrayCell(hArray, GetRandomInt(0, GetArraySize(hArray) - 1)); - } - - CloseHandle(hArray); - } - -#if defined DEBUG - if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d): array size 1 = %d, array size 2 = %d", iBossIndex, iArraySize, iArraySize2); -#endif - - if (iBestPlayer <= 0) - { -#if defined DEBUG - if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d) failed: no ibestPlayer!", iBossIndex); -#endif - return false; - } - - // Determine the distance we can appear from the player. - new Float:flPercentFar = 0.75 * (1.0 - flPercent); - new Float:flPercentAverage = 0.6 * (1.0 - flPercent); - //new Float:flPercentClose = 1.0 - flPercentFar - flPercentAverage; - - new Float:flUpperBoundFar = flPercentFar; - new Float:flUpperBoundAverage = flPercentFar + flPercentAverage; - //new Float:flUpperBoundClose = 1.0; - - new iRange = 1; - new Float:flChance = GetRandomFloat(0.0, 1.0); - new Float:flMaxRangeN = GetProfileFloat(sProfile, "teleport_range_max"); - new Float:flMinRangeN = GetProfileFloat(sProfile, "teleport_range_min"); - - new bool:bVisiblePls = false; - new bool:bBeCreepy = false; - - if (!bProxy) - { - // Are we gonna teleport in front of a player this time? - if (GetProfileNum(sProfile, "teleport_ignorevis_enable")) - { - if (GetRandomFloat(0.0, 1.0) < GetProfileFloat(sProfile, "teleport_ignorevis_chance") * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier) - { - bVisiblePls = true; - } - - if (GetRandomFloat(0.0, 1.0) < GetProfileFloat(sProfile, "teleport_creepy_chance", 0.33)) - { - bBeCreepy = true; - } - } - } - - new Float:flMaxRange = flMaxRangeN; - new Float:flMinRange = flMinRangeN; - - if (bVisiblePls) - { - flMaxRange = GetProfileFloat(sProfile, "teleport_ignorevis_range_max", flMaxRangeN); - flMinRange = GetProfileFloat(sProfile, "teleport_ignorevis_range_min", flMinRangeN); - } - - // Get distances. - new Float:flDistanceFar = GetRandomFloat(flMaxRange * 0.75, flMaxRange); - if (flDistanceFar < flMinRange) flDistanceFar = flMinRange; - new Float:flDistanceAverage = GetRandomFloat(flMaxRange * 0.33, flMaxRange * 0.75); - if (flDistanceAverage < flMinRange) flDistanceAverage = flMinRange; - new Float:flDistanceClose = GetRandomFloat(0.0, flMaxRange * 0.33); - if (flDistanceClose < flMinRange) flDistanceClose = flMinRange; - - if (flChance >= 0.0 && flChance < flUpperBoundFar) iRange = 1; - else if (flChance >= flUpperBoundFar && flChance < flUpperBoundAverage) iRange = 2; - else if (flChance >= flUpperBoundAverage) iRange = 3; - - // Get a circle of positions around the player that we can appear in. - - // Create arrays first. - new Handle:hArrayFar = CreateArray(3); - new Handle:hArrayAverage = CreateArray(3); - new Handle:hArrayClose = CreateArray(3); - - // Set up our distances array. - decl Float:flDistances[3]; - flDistances[0] = flDistanceFar; - flDistances[1] = flDistanceAverage; - flDistances[2] = flDistanceClose; - - decl Float:hisEyePos[3], Float:hisEyeAng[3], Float:tempPos[3], Float:tempDir[3], Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; - GetClientEyePosition(iBestPlayer, hisEyePos); - GetClientEyeAngles(iBestPlayer, hisEyeAng); - - decl Handle:hTrace, index, Float:flHitNormal[3]; - decl Handle:hArray; - - decl Float:flTargetMins[3], Float:flTargetMaxs[3]; - if (!bProxy) - { - for (new i = 0; i < 3; i++) - { - flTargetMins[i] = g_flSlenderDetectMins[iBossIndex][i]; - flTargetMaxs[i] = g_flSlenderDetectMaxs[iBossIndex][i]; - } - } - else - { - GetEntPropVector(iProxyPlayer, Prop_Send, "m_vecMins", flTargetMins); - GetEntPropVector(iProxyPlayer, Prop_Send, "m_vecMaxs", flTargetMaxs); - } - - for (new i = 0; i < iRange; i++) - { - for (new Float:addAng = 0.0; addAng < 360.0; addAng += 7.5) - { - tempDir[0] = 0.0; - tempDir[1] = hisEyeAng[1] + addAng; - tempDir[2] = 0.0; - - GetAngleVectors(tempDir, tempDir, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(tempDir, tempDir); - ScaleVector(tempDir, flDistances[i]); - AddVectors(tempDir, hisEyePos, tempPos); - - // Drop to the ground if we're above ground using a TraceHull so IsSpaceOccupiedNPC can return true on something. - hTrace = TR_TraceRayFilterEx(tempPos, Float:{ 90.0, 0.0, 0.0 }, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitCharactersOrEntity, iBestPlayer); - TR_GetEndPosition(flBuffer, hTrace); - CloseHandle(hTrace); - - flBuffer2[0] = flTargetMins[0]; - flBuffer2[1] = flTargetMins[1]; - flBuffer2[2] = -flTargetMaxs[2]; - flBuffer3[0] = flTargetMaxs[0]; - flBuffer3[1] = flTargetMaxs[1]; - flBuffer3[2] = -flTargetMins[0]; - - if (GetVectorDistance(tempPos, flBuffer) >= 300.0) continue; - - // Drop dowwwwwn. - hTrace = TR_TraceHullFilterEx(tempPos, flBuffer, flBuffer2, flBuffer3, MASK_NPCSOLID, TraceRayDontHitCharactersOrEntity, iBestPlayer); - TR_GetEndPosition(tempPos, hTrace); - TR_GetPlaneNormal(hTrace, flHitNormal); - CloseHandle(hTrace); - - GetVectorAngles(flHitNormal, flHitNormal); - for (new i2 = 0; i2 < 3; i2++) flHitNormal[i2] = AngleNormalize(flHitNormal[i2]); - - tempPos[2] -= g_flSlenderDetectMaxs[iBossIndex][2]; - - if (TR_PointOutsideWorld(tempPos) - || (IsSpaceOccupiedNPC(tempPos, flTargetMins, flTargetMaxs, NPCGetEntIndex(iBossIndex))) - || (bProxy && IsSpaceOccupiedPlayer(tempPos, flTargetMins, flTargetMaxs, iProxyPlayer)) - || (flHitNormal[0] >= 0.0 && flHitNormal[0] < 45.0) - || (flHitNormal[0] < 0.0 && flHitNormal[0] > -45.0)) - { - continue; - } - - // Check if this position isn't too close to anyone else. - new bool:bTooClose = false; - - for (new i2 = 1; i2 <= MaxClients; i2++) - { - if (!IsClientInGame(i2) || !IsPlayerAlive(i2) || g_bPlayerEliminated[i2] || IsClientInGhostMode(i2)) continue; - GetClientAbsOrigin(i2, flBuffer); - if (GetVectorDistance(flBuffer, tempPos) < flMinRange) - { - bTooClose = true; - break; - } - } - - // Check if this position is too close to a boss. - if (!bTooClose) - { - decl iSlender; - for (new i2 = 0; i2 < MAX_BOSSES; i2++) - { - if (i2 == iBossIndex) continue; - if (NPCGetUniqueID(i2) == -1) continue; - - // If I'm a main boss, only check the distance between my copies and me. - if (g_iSlenderCopyMaster[iBossIndex] == -1) - { - if (g_iSlenderCopyMaster[i2] != iBossIndex) continue; - } - // If I'm a copy, just check with my other copy friends and my main boss. - else - { - new iMyMaster = g_iSlenderCopyMaster[iBossIndex]; - if (g_iSlenderCopyMaster[i2] != iMyMaster || i2 != iMyMaster) continue; - } - - iSlender = NPCGetEntIndex(i2); - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; - - SlenderGetAbsOrigin(i2, flBuffer); - if (GetVectorDistance(flBuffer, tempPos) < GetProfileFloat(sProfile, "teleport_dist_from_other_copies", 800.0)) - { - bTooClose = true; - break; - } - } - } - - if (bTooClose) continue; - - // Check from top to bottom of me. - - new bool:bCheckBlink = bool:GetProfileNum(sProfile, "teleport_use_blink"); - - // Check if my copy master or my fellow copies could see this position. - new bool:bDontAddPosition = false; - new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); - - decl Float:flCopyCheckPositions[6]; - for (new i2 = 0; i2 < 3; i2++) flCopyCheckPositions[i2] = tempPos[i2]; - for (new i2 = 3; i2 < 6; i2++) flCopyCheckPositions[i2] = tempPos[i2 - 3] + g_flSlenderEyePosOffset[iBossIndex][i2 - 3]; - - for (new i2 = 0; i2 < 2; i2++) - { - decl Float:flCopyCheckPos[3]; - for (new i3 = 0; i3 < 3; i3++) flCopyCheckPos[i3] = flCopyCheckPositions[i3 + (3 * i2)]; - - // Check the conditions first. - if (bVisiblePls) - { - if (!IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink) && - !IsPointVisibleToPlayer(iBestPlayer, flCopyCheckPos, _, bCheckBlink)) - { - bDontAddPosition = true; - break; - } - } - else if (bBeCreepy) - { - if (!IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink) && - IsPointVisibleToAPlayer(flCopyCheckPos, false, bCheckBlink) && - IsPointVisibleToPlayer(iBestPlayer, flCopyCheckPos, false, bCheckBlink)) - { - // Do nothing. - } - else - { - continue; - } - } - else - { - if (IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink)) - { - bDontAddPosition = true; - break; - } - } - - for (new i3 = 0; i3 < MAX_BOSSES; i3++) - { - if (i3 == iBossIndex) continue; - if (NPCGetUniqueID(i3) == -1) continue; - - new iBoss = NPCGetEntIndex(i3); - if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; - - if (i3 == iCopyMaster || - (iCopyMaster != -1 && NPCGetFromUniqueID(g_iSlenderCopyMaster[i3]) == iCopyMaster)) - { - } - else continue; - - decl Float:flCopyPos[3]; - SlenderGetEyePosition(i3, flCopyPos); - hTrace = TR_TraceRayFilterEx(flCopyPos, - flCopyCheckPos, - CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, - RayType_EndPoint, - TraceRayBossVisibility, - iBoss); - - bDontAddPosition = !TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (!bDontAddPosition) - { - decl Float:flCopyMins[3], Float:flCopyMaxs[3]; - GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flCopyPos); - GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flCopyMins); - GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flCopyMaxs); - - for (new i4 = 0; i4 < 3; i4++) flCopyPos[i4] += ((flCopyMins[i4] + flCopyMaxs[i4]) / 2.0); - - hTrace = TR_TraceRayFilterEx(flCopyPos, - flCopyCheckPos, - CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, - RayType_EndPoint, - TraceRayBossVisibility, - iBoss); - - bDontAddPosition = !TR_DidHit(hTrace); - CloseHandle(hTrace); - } - - if (bDontAddPosition) break; - } - - if (bDontAddPosition) break; - } - - if (bDontAddPosition) continue; - - // Insert the vector into our array. Choose which one, first. - // We're just using hArray as a variable to store the correct array, not the array itself. All arrays will be closed at the end. - if (i == 0) hArray = hArrayFar; - else if (i == 1) hArray = hArrayAverage; - else if (i == 2) hArray = hArrayClose; - - index = PushArrayCell(hArray, tempPos[0]); - SetArrayCell(hArray, index, tempPos[1], 1); - SetArrayCell(hArray, index, tempPos[2], 2); - } - } - - new size; - if ((size = GetArraySize(hArrayClose)) > 0) - { - index = GetRandomInt(0, size - 1); - buffer[0] = Float:GetArrayCell(hArrayClose, index); - buffer[1] = Float:GetArrayCell(hArrayClose, index, 1); - buffer[2] = Float:GetArrayCell(hArrayClose, index, 2); - } - else if ((size = GetArraySize(hArrayAverage)) > 0) - { - index = GetRandomInt(0, size - 1); - buffer[0] = Float:GetArrayCell(hArrayAverage, index); - buffer[1] = Float:GetArrayCell(hArrayAverage, index, 1); - buffer[2] = Float:GetArrayCell(hArrayAverage, index, 2); - } - else if ((size = GetArraySize(hArrayFar)) > 0) - { - index = GetRandomInt(0, size - 1); - buffer[0] = Float:GetArrayCell(hArrayFar, index); - buffer[1] = Float:GetArrayCell(hArrayFar, index, 1); - buffer[2] = Float:GetArrayCell(hArrayFar, index, 2); - } - else - { - CloseHandle(hArrayClose); - CloseHandle(hArrayAverage); - CloseHandle(hArrayFar); - -#if defined DEBUG - if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d) failed: no locations available", iBossIndex); -#endif - - return false; - } - - CloseHandle(hArrayClose); - CloseHandle(hArrayAverage); - CloseHandle(hArrayFar); - return true; -} - -bool:SlenderMarkAsFake(iBossIndex) -{ - new iBossFlags = NPCGetFlags(iBossIndex); - if (iBossFlags & SFF_MARKEDASFAKE) return false; - - new slender = NPCGetEntIndex(iBossIndex); - new iSlenderModel = EntRefToEntIndex(g_iSlenderModel[iBossIndex]); - g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; - g_iSlenderModel[iBossIndex] = INVALID_ENT_REFERENCE; - - NPCSetFlags(iBossIndex, iBossFlags | SFF_MARKEDASFAKE); - - g_hSlenderFakeTimer[iBossIndex] = CreateTimer(3.0, Timer_SlenderMarkedAsFake, iBossIndex, TIMER_FLAG_NO_MAPCHANGE); - - if (slender && slender != INVALID_ENT_REFERENCE) - { - CreateTimer(2.0, Timer_KillEntity, EntIndexToEntRef(slender), TIMER_FLAG_NO_MAPCHANGE); - - new iFlags = GetEntProp(slender, Prop_Send, "m_usSolidFlags"); - if (!(iFlags & 0x0004)) iFlags |= 0x0004; // FSOLID_NOT_SOLID - if (!(iFlags & 0x0008)) iFlags |= 0x0008; // FSOLID_TRIGGER - SetEntProp(slender, Prop_Send, "m_usSolidFlags", iFlags); - } - - if (iSlenderModel && iSlenderModel != INVALID_ENT_REFERENCE) - { - SetVariantFloat(0.0); - AcceptEntityInput(iSlenderModel, "SetPlaybackRate"); - SetEntityRenderFx(iSlenderModel, RENDERFX_FADE_FAST); - } - - return true; -} - -public Action:Timer_SlenderMarkedAsFake(Handle:timer, any:data) -{ - if (timer != g_hSlenderFakeTimer[data]) return; - - NPCRemove(data); -} - -stock SpawnSlenderModel(iBossIndex, const Float:pos[3]) -{ - if (NPCGetUniqueID(iBossIndex) == -1) - { - LogError("Could not spawn boss model: boss does not exist!"); - return -1; - } - - new iProfileIndex = NPCGetProfileIndex(iBossIndex); - - decl String:buffer[PLATFORM_MAX_PATH], String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - GetProfileString(sProfile, "model", buffer, sizeof(buffer)); - if (!buffer[0]) - { - LogError("Could not spawn boss model: model is invalid!"); - return -1; - } - - new Float:flModelScale = NPCGetModelScale(iBossIndex); - if (flModelScale <= 0.0) - { - LogError("Could not spawn boss model: model scale is less than or equal to 0.0!"); - return -1; - } - - new iSlenderModel = CreateEntityByName("prop_dynamic_override"); - if (iSlenderModel != -1) - { - SetEntityModel(iSlenderModel, buffer); - - TeleportEntity(iSlenderModel, pos, NULL_VECTOR, NULL_VECTOR); - DispatchSpawn(iSlenderModel); - ActivateEntity(iSlenderModel); - - SetEntProp(iSlenderModel, Prop_Send, "m_nSkin", GetBossProfileSkin(iProfileIndex)); - SetEntProp(iSlenderModel, Prop_Send, "m_nBody", GetBossProfileBodyGroups(iProfileIndex)); - - GetProfileString(sProfile, "animation_idle", buffer, sizeof(buffer)); - if (buffer[0]) - { - SetVariantString(buffer); - AcceptEntityInput(iSlenderModel, "SetDefaultAnimation"); - SetVariantString(buffer); - AcceptEntityInput(iSlenderModel, "SetAnimation"); - AcceptEntityInput(iSlenderModel, "DisableCollision"); - } - - SetVariantFloat(GetProfileFloat(sProfile, "animation_idle_playbackrate", 1.0)); - AcceptEntityInput(iSlenderModel, "SetPlaybackRate"); - - SetEntPropFloat(iSlenderModel, Prop_Send, "m_flModelScale", flModelScale); - - // Create special effects. - SetEntityRenderMode(iSlenderModel, RenderMode:GetProfileNum(sProfile, "effect_rendermode", _:RENDER_NORMAL)); - SetEntityRenderFx(iSlenderModel, RenderFx:GetProfileNum(sProfile, "effect_renderfx", _:RENDERFX_NONE)); - - decl iColor[4]; - GetProfileColor(sProfile, "effect_rendercolor", iColor[0], iColor[1], iColor[2], iColor[3]); - SetEntityRenderColor(iSlenderModel, iColor[0], iColor[1], iColor[2], iColor[3]); - - KvRewind(g_hConfig); - if (KvJumpToKey(g_hConfig, sProfile) && - KvJumpToKey(g_hConfig, "effects") && - KvGotoFirstSubKey(g_hConfig)) - { - do - { - - } - while KvGotoNextKey(g_hConfig); - } - } - - return iSlenderModel; -} - -stock bool:PlayerCanSeeSlender(client, iBossIndex, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) -{ - return IsNPCVisibleToPlayer(iBossIndex, client, bCheckFOV, bCheckBlink, bCheckEliminated); -} - -stock bool:PeopleCanSeeSlender(iBossIndex, bool:bCheckFOV=true, bool:bCheckBlink=false) -{ - return IsNPCVisibleToAPlayer(iBossIndex, bCheckFOV, bCheckBlink); -} - -// TODO: bCheckBlink and bCheckEliminated should NOT be function arguments! -bool:IsNPCVisibleToPlayer(iNPCIndex, client, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) -{ - if (!NPCIsValid(iNPCIndex)) return false; - - new iNPC = NPCGetEntIndex(iNPCIndex); - if (iNPC && iNPC != INVALID_ENT_REFERENCE) - { - decl Float:flEyePos[3]; - NPCGetEyePosition(iNPCIndex, flEyePos); - return IsPointVisibleToPlayer(client, flEyePos, bCheckFOV, bCheckBlink, bCheckEliminated); - } - - return false; -} - -// TODO: bCheckBlink and bCheckEliminated should NOT be function arguments! -bool:IsNPCVisibleToAPlayer(iNPCIndex, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) -{ - for (new client = 1; client <= MaxClients; client++) - { - if (IsNPCVisibleToPlayer(iNPCIndex, client, bCheckFOV, bCheckBlink, bCheckEliminated)) - { - return true; - } - } - - return false; -} - -Float:NPCGetDistanceFromPoint(iNPCIndex, const Float:flPoint[3], bool:bSquared=false) -{ - new iNPC = NPCGetEntIndex(iNPCIndex); - if (iNPC && iNPC != INVALID_ENT_REFERENCE) - { - decl Float:flPos[3]; - SlenderGetAbsOrigin(iNPCIndex, flPos); - - return GetVectorDistance(flPos, flPoint, bSquared); - } - - return -1.0; -} - -Float:NPCGetDistanceFromEntity(iNPCIndex, ent, bool:bSquared=false) -{ - if (!IsValidEntity(ent)) return -1.0; - - decl Float:flPos[3]; - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); - - return NPCGetDistanceFromPoint(iNPCIndex, flPos, bSquared); -} - -public bool:TraceRayBossVisibility(entity, mask, any:data) -{ - if (entity == data || IsValidClient(entity)) return false; - - new iBossIndex = NPCGetFromEntIndex(entity); - if (iBossIndex != -1) return false; - - if (IsValidEdict(entity)) - { - decl String:sClass[64]; - GetEntityNetClass(entity, sClass, sizeof(sClass)); - - if (StrEqual(sClass, "CTFAmmoPack")) return false; - } - - return true; -} - -public bool:TraceRayDontHitCharacters(entity, mask, any:data) -{ - if (entity > 0 && entity <= MaxClients) return false; - - new iBossIndex = NPCGetFromEntIndex(entity); - if (iBossIndex != -1) return false; - - return true; -} - -public bool:TraceRayDontHitCharactersOrEntity(entity, mask, any:data) -{ - if (entity == data) return false; - - if (entity > 0 && entity <= MaxClients) return false; - - new iBossIndex = NPCGetFromEntIndex(entity); - if (iBossIndex != -1) return false; - - return true; -} - -#include "rytp_horror/npc/npc_chaser.sp" +#if defined _sf2_npc_included + #endinput +#endif +#define _sf2_npc_included + +#define SF2_BOSS_PAGE_CALCULATION 0.3 +#define SF2_BOSS_COPY_SPAWN_MIN_DISTANCE 1850.0 // The default minimum distance boss copies can spawn from each other. + +#define SF2_BOSS_ATTACK_MELEE 0 + +static g_iNPCGlobalUniqueID = 0; + +static g_iNPCUniqueID[MAX_BOSSES] = { -1, ... }; +static String:g_strSlenderProfile[MAX_BOSSES][SF2_MAX_PROFILE_NAME_LENGTH]; +static g_iNPCProfileIndex[MAX_BOSSES] = { -1, ... }; +static g_iNPCUniqueProfileIndex[MAX_BOSSES] = { -1, ... }; +static g_iNPCType[MAX_BOSSES] = { SF2BossType_Unknown, ... }; +static g_iNPCFlags[MAX_BOSSES] = { 0, ... }; +static Float:g_flNPCModelScale[MAX_BOSSES] = { 1.0, ... }; + +static Float:g_flNPCFieldOfView[MAX_BOSSES] = { 0.0, ... }; +static Float:g_flNPCTurnRate[MAX_BOSSES] = { 0.0, ... }; + +static g_iSlender[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; + +static Float:g_flNPCSpeed[MAX_BOSSES][Difficulty_Max]; +static Float:g_flNPCMaxSpeed[MAX_BOSSES][Difficulty_Max]; + +static Float:g_flNPCScareRadius[MAX_BOSSES]; +static Float:g_flNPCScareCooldown[MAX_BOSSES]; + +static g_iNPCTeleportType[MAX_BOSSES] = { -1, ... }; + +static Float:g_flNPCAnger[MAX_BOSSES] = { 1.0, ... }; +static Float:g_flNPCAngerAddOnPageGrab[MAX_BOSSES] = { 0.0, ... }; +static Float:g_flNPCAngerAddOnPageGrabTimeDiff[MAX_BOSSES] = { 0.0, ... }; + +static Float:g_flNPCSearchRadius[MAX_BOSSES] = { 0.0, ... }; +static Float:g_flNPCInstantKillRadius[MAX_BOSSES] = { 0.0, ... }; + +static bool:g_bNPCDeathCamEnabled[MAX_BOSSES] = { false, ... }; + +static g_iNPCEnemy[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; + +#if defined METHODMAPS + +const SF2NPC_BaseNPC SF2_INVALID_NPC = SF2NPC_BaseNPC:-1; + +methodmap SF2NPC_BaseNPC +{ + property int Index + { + public get() { return _:this; } + } + + property int Type + { + public get() { return NPCGetType(this.Index); } + } + + property int ProfileIndex + { + public get() { return NPCGetProfileIndex(this.Index); } + } + + property int UniqueProfileIndex + { + public get() { return NPCGetUniqueProfileIndex(this.Index); } + } + + property int EntRef + { + public get() { return NPCGetEntRef(this.Index); } + } + + property int EntIndex + { + public get() { return NPCGetEntIndex(this.Index); } + } + + property int Flags + { + public get() { return NPCGetFlags(this.Index); } + public set(int flags) { NPCSetFlags(this.Index); } + } + + property float ModelScale + { + public get() { return NPCGetModelScale(this.Index) }; + } + + property float TurnRate + { + public get() { return NPCGetTurnRate(this.Index) }; + } + + property float FOV + { + public get() { return NPCGetFOV(this.Index); } + } + + property float Anger + { + public get() { return NPCGetAnger(this.Index); } + public set(float amount) { NPCSetAnger(this.Index, amount); } + } + + property float AngerAddOnPageGrab + { + public get() { return NPCGetAngerAddOnPageGrab(this.Index); } + } + + property float AngerAddOnPageGrabTimeDiff + { + public get() { return NPCGetAngerAddOnPageGrabTimeDiff(this.Index); } + } + + property float SearchRadius + { + public get() { return NPCGetSearchRadius(this.Index); } + } + + property float ScareRadius + { + public get() { return NPCGetScareRadius(this.Index); } + } + + property float ScareCooldown + { + public get() { return NPCGetScareCooldown(this.Index); } + } + + property float InstantKillRadius + { + public get() { return NPCGetInstantKillRadius(this.Index); } + } + + property int TeleportType + { + public get() { return NPCGetTeleportType(this.Index); } + } + + property int Enemy + { + public get() { return NPCGetEnemy(this.Index); } + public set(int entIndex) { NPCSetEnemy(this.Index, entIndex); } + } + + property bool DeathCamEnabled + { + public get() { return NPCHasDeathCamEnabled(this.Index); } + public set(bool state) { NPCSetDeathCamEnabled(this.Index, state); } + } + + public SF2NPC_BaseNPC(int index) + { + return SF2NPC_BaseNPC:index; + } + + public ~SF2NPC_BaseNPC() + { + NPCRemove(this.Index); + } + + public bool IsValid() + { + return NPCIsValid(this.Index); + } + + public void GetProfile(char[] buffer, int bufferlen) + { + NPCGetProfile(this.Index, buffer, bufferlen); + } + + public void SetProfile(const char[] profileName) + { + NPCSetProfile(this.Index, profileName); + } + + public float GetSpeed(int difficulty) + { + return NPCGetSpeed(this.Index, difficulty); + } + + public float GetMaxSpeed(int difficulty) + { + return NPCGetMaxSpeed(this.Index, difficulty); + } + + public void GetEyePosition(float buffer[3], const float defaultValue[3] = { 0.0, 0.0, 0.0 }) + { + NPCGetEyePosition(this.Index, buffer, defaultValue); + } + + public void GetEyePositionOffset(float buffer[3]) + { + NPCGetEyePositionOffset(this.Index, buffer); + } + + public void AddAnger(float amount) + { + NPCAddAnger(this.Index, amount); + } + + public bool HasAttribute(const char[] attributeName) + { + return NPCHasAttribute(this.Index, attributeName); + } + + public float GetAttributeValue(const char[] attributeName, float defaultValue = 0.0) + { + return NPCGetAttributeValue(this.Index, attributeName, defaultValue); + } +} + +#endif + +bool:NPCHasDeathCamEnabled(iNPCIndex) +{ + return g_bNPCDeathCamEnabled[iNPCIndex]; +} + +NPCSetDeathCamEnabled(iNPCIndex, bool:state) +{ + g_bNPCDeathCamEnabled[iNPCIndex] = state; +} + +public NPCInitialize() +{ + NPCChaserInitialize(); +} + +public NPCOnConfigsExecuted() +{ + g_iNPCGlobalUniqueID = 0; +} + +bool:NPCIsValid(iNPCIndex) +{ + return bool:(iNPCIndex >= 0 && iNPCIndex < MAX_BOSSES && NPCGetUniqueID(iNPCIndex) != -1); +} + +NPCGetUniqueID(iNPCIndex) +{ + return g_iNPCUniqueID[iNPCIndex]; +} + +NPCGetFromUniqueID(iNPCUniqueID) +{ + if (iNPCUniqueID == -1) return -1; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == iNPCUniqueID) + { + return i; + } + } + + return -1; +} + +NPCGetEntRef(iNPCIndex) +{ + return g_iSlender[iNPCIndex]; +} + +NPCGetEntIndex(iNPCIndex) +{ + return EntRefToEntIndex(NPCGetEntRef(iNPCIndex)); +} + +NPCGetFromEntIndex(entity) +{ + if (!entity || !IsValidEntity(entity)) return -1; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetEntIndex(i) == entity) + { + return i; + } + } + + return -1; +} + +NPCGetCount() +{ + new iCount; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + if (NPCGetFlags(i) & SFF_FAKE) continue; + + iCount++; + } + + return iCount; +} + +NPCGetProfileIndex(iNPCIndex) +{ + return g_iNPCProfileIndex[iNPCIndex]; +} + +NPCGetUniqueProfileIndex(iNPCIndex) +{ + return g_iNPCUniqueProfileIndex[iNPCIndex]; +} + +bool:NPCGetProfile(iNPCIndex, String:buffer[], bufferlen) +{ + strcopy(buffer, bufferlen, g_strSlenderProfile[iNPCIndex]); + return true; +} + +NPCSetProfile(iNPCIndex, const String:sProfile[]) +{ + strcopy(g_strSlenderProfile[iNPCIndex], sizeof(g_strSlenderProfile[]), sProfile); +} + +NPCRemove(iNPCIndex) +{ + if (!NPCIsValid(iNPCIndex)) return; + + RemoveProfile(iNPCIndex); +} + +NPCRemoveAll() +{ + for (new i = 0; i < MAX_BOSSES; i++) + { + NPCRemove(i); + } +} + +NPCGetType(iNPCIndex) +{ + return g_iNPCType[iNPCIndex]; +} + +NPCGetFlags(iNPCIndex) +{ + return g_iNPCFlags[iNPCIndex]; +} + +NPCSetFlags(iNPCIndex, iFlags) +{ + g_iNPCFlags[iNPCIndex] = iFlags; +} + +Float:NPCGetModelScale(iNPCIndex) +{ + return g_flNPCModelScale[iNPCIndex]; +} + +Float:NPCGetSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCSpeed[iNPCIndex][iDifficulty]; +} + +Float:NPCGetMaxSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCMaxSpeed[iNPCIndex][iDifficulty]; +} + +Float:NPCGetTurnRate(iNPCIndex) +{ + return g_flNPCTurnRate[iNPCIndex]; +} + +Float:NPCGetFOV(iNPCIndex) +{ + return g_flNPCFieldOfView[iNPCIndex]; +} + +Float:NPCGetAnger(iNPCIndex) +{ + return g_flNPCAnger[iNPCIndex]; +} + +NPCSetAnger(iNPCIndex, Float:flAnger) +{ + g_flNPCAnger[iNPCIndex] = flAnger; +} + +NPCAddAnger(iNPCIndex, Float:flAmount) +{ + g_flNPCAnger[iNPCIndex] += flAmount; +} + +Float:NPCGetAngerAddOnPageGrab(iNPCIndex) +{ + return g_flNPCAngerAddOnPageGrab[iNPCIndex]; +} + +Float:NPCGetAngerAddOnPageGrabTimeDiff(iNPCIndex) +{ + return g_flNPCAngerAddOnPageGrabTimeDiff[iNPCIndex]; +} + +NPCGetEyePositionOffset(iNPCIndex, Float:buffer[3]) +{ + buffer[0] = g_flSlenderEyePosOffset[iNPCIndex][0]; + buffer[1] = g_flSlenderEyePosOffset[iNPCIndex][1]; + buffer[2] = g_flSlenderEyePosOffset[iNPCIndex][2]; +} + +Float:NPCGetSearchRadius(iNPCIndex) +{ + return g_flNPCSearchRadius[iNPCIndex]; +} + +Float:NPCGetScareRadius(iNPCIndex) +{ + return g_flNPCScareRadius[iNPCIndex]; +} + +Float:NPCGetScareCooldown(iNPCIndex) +{ + return g_flNPCScareCooldown[iNPCIndex]; +} + +Float:NPCGetInstantKillRadius(iNPCIndex) +{ + return g_flNPCInstantKillRadius[iNPCIndex]; +} + +NPCGetTeleportType(iNPCIndex) +{ + return g_iNPCTeleportType[iNPCIndex]; +} + +NPCGetEnemy(iNPCIndex) +{ + return g_iNPCEnemy[iNPCIndex]; +} + +NPCSetEnemy(iNPCIndex, ent) +{ + g_iNPCEnemy[iNPCIndex] = IsValidEntity(ent) ? EntIndexToEntRef(ent) : INVALID_ENT_REFERENCE; +} + +/** + * Returns the boss's eye position (eye pos offset + absorigin). + */ +bool:NPCGetEyePosition(iNPCIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) +{ + buffer[0] = flDefaultValue[0]; + buffer[1] = flDefaultValue[1]; + buffer[2] = flDefaultValue[2]; + + if (!NPCIsValid(iNPCIndex)) return false; + + new iNPC = NPCGetEntIndex(iNPCIndex); + if (!iNPC || iNPC == INVALID_ENT_REFERENCE) return false; + + // @TODO: Replace SlenderGetAbsOrigin with GetEntPropVector + decl Float:flPos[3], Float:flEyePosOffset[3]; + SlenderGetAbsOrigin(iNPCIndex, flPos); + NPCGetEyePositionOffset(iNPCIndex, flEyePosOffset); + + AddVectors(flPos, flEyePosOffset, buffer); + return true; +} + +bool:NPCHasAttribute(iNPCIndex, const String:sAttribute[]) +{ + if (NPCGetUniqueID(iNPCIndex) == -1) return false; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iNPCIndex, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + if (!KvJumpToKey(g_hConfig, "attributes")) return false; + + return KvJumpToKey(g_hConfig, sAttribute); +} + +Float:NPCGetAttributeValue(iNPCIndex, const String:sAttribute[], Float:flDefaultValue=0.0) +{ + if (!NPCHasAttribute(iNPCIndex, sAttribute)) return flDefaultValue; + return KvGetFloat(g_hConfig, "value", flDefaultValue); +} + +bool:SlenderCanRemove(iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) return false; + + if (PeopleCanSeeSlender(iBossIndex, _, false)) return false; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iTeleportType = GetProfileNum(sProfile, "teleport_type"); + + switch (iTeleportType) + { + case 0: + { + if (GetProfileNum(sProfile, "static_on_radius")) + { + decl Float:flSlenderPos[3], Float:flBuffer[3]; + SlenderGetAbsOrigin(iBossIndex, flSlenderPos); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + IsClientInGhostMode(i) || + IsClientInDeathCam(i)) continue; + + if (!IsPointVisibleToPlayer(i, flSlenderPos, false, false)) continue; + + GetClientAbsOrigin(i, flBuffer); + if (GetVectorDistance(flBuffer, flSlenderPos) <= GetProfileFloat(sProfile, "static_radius")) + { + return false; + } + } + } + } + case 1: + { + if (PeopleCanSeeSlender(iBossIndex, _, SlenderUsesBlink(iBossIndex)) || PeopleCanSeeSlender(iBossIndex, false, false)) + { + return false; + } + } + case 2: + { + new iState = g_iSlenderState[iBossIndex]; + if (iState == STATE_IDLE || iState == STATE_WANDER) + { + if (GetGameTime() < g_flSlenderTimeUntilKill[iBossIndex]) + { + return false; + } + } + else + { + return false; + } + } + } + + return true; +} + +bool:SlenderGetAbsOrigin(iBossIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) +{ + for (new i = 0; i < 3; i++) buffer[i] = flDefaultValue[i]; + + if (iBossIndex < 0 || NPCGetUniqueID(iBossIndex) == -1) return false; + + new slender = NPCGetEntIndex(iBossIndex); + if (!slender || slender == INVALID_ENT_REFERENCE) return false; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl Float:flPos[3], Float:flOffset[3]; + GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flPos); + GetProfileVector(sProfile, "pos_offset", flOffset, flDefaultValue); + SubtractVectors(flPos, flOffset, buffer); + + return true; +} + +bool:SlenderGetEyePosition(iBossIndex, Float:buffer[3], const Float:flDefaultValue[3]={ 0.0, 0.0, 0.0 }) +{ + return NPCGetEyePosition(iBossIndex, buffer, flDefaultValue); +} + +bool:SelectProfile(iBossIndex, const String:sProfile[], iAdditionalBossFlags=0, iCopyMaster=-1, bool:bSpawnCompanions=true, bool:bPlaySpawnSound=true) +{ + if (!IsProfileValid(sProfile)) + { + LogSF2Message("Could not select profile for boss %d: profile %s is invalid!", iBossIndex, sProfile); + return false; + } + + NPCRemove(iBossIndex); + + new iProfileIndex = GetBossProfileIndexFromName(sProfile); + new iUniqueProfileIndex = GetBossProfileUniqueProfileIndex(iProfileIndex); + + NPCSetProfile(iBossIndex, sProfile); + + new iBossType = GetBossProfileType(iProfileIndex); + + g_iNPCProfileIndex[iBossIndex] = iProfileIndex; + g_iNPCUniqueProfileIndex[iBossIndex] = iUniqueProfileIndex; + g_iNPCUniqueID[iBossIndex] = g_iNPCGlobalUniqueID++; + g_iNPCType[iBossIndex] = iBossType; + + g_flNPCModelScale[iBossIndex] = GetBossProfileModelScale(iProfileIndex); + + NPCSetFlags(iBossIndex, GetBossProfileFlags(iProfileIndex) | iAdditionalBossFlags); + + GetBossProfileEyePositionOffset(iProfileIndex, g_flSlenderEyePosOffset[iBossIndex]); + GetBossProfileEyeAngleOffset(iProfileIndex, g_flSlenderEyeAngOffset[iBossIndex]); + + GetProfileVector(sProfile, "mins", g_flSlenderDetectMins[iBossIndex]); + GetProfileVector(sProfile, "maxs", g_flSlenderDetectMaxs[iBossIndex]); + + NPCSetAnger(iBossIndex, GetBossProfileAngerStart(iProfileIndex)); + g_flNPCAngerAddOnPageGrab[iBossIndex] = GetBossProfileAngerAddOnPageGrab(iProfileIndex); + g_flNPCAngerAddOnPageGrabTimeDiff[iBossIndex] = GetBossProfileAngerPageGrabTimeDiff(iProfileIndex); + + g_iSlenderCopyMaster[iBossIndex] = -1; + g_iSlenderHealth[iBossIndex] = GetProfileNum(sProfile, "health", 900); + + for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) + { + g_flNPCSpeed[iBossIndex][iDifficulty] = GetBossProfileSpeed(iProfileIndex, iDifficulty); + g_flNPCMaxSpeed[iBossIndex][iDifficulty] = GetBossProfileMaxSpeed(iProfileIndex, iDifficulty); + } + + g_flNPCTurnRate[iBossIndex] = GetBossProfileTurnRate(iProfileIndex); + g_flNPCFieldOfView[iBossIndex] = GetBossProfileFOV(iProfileIndex); + + g_flNPCSearchRadius[iBossIndex] = GetBossProfileSearchRadius(iProfileIndex); + + g_flNPCScareRadius[iBossIndex] = GetBossProfileScareRadius(iProfileIndex); + g_flNPCScareCooldown[iBossIndex] = GetBossProfileScareCooldown(iProfileIndex); + + g_flNPCInstantKillRadius[iBossIndex] = GetBossProfileInstantKillRadius(iProfileIndex); + + g_iNPCTeleportType[iBossIndex] = GetBossProfileTeleportType(iProfileIndex); + + g_iNPCEnemy[iBossIndex] = INVALID_ENT_REFERENCE; + + // Deathcam values. + NPCSetDeathCamEnabled(iBossIndex, bool:GetProfileNum(sProfile, "death_cam")); + + g_flSlenderAcceleration[iBossIndex] = GetProfileFloat(sProfile, "acceleration", 150.0); + g_hSlenderFakeTimer[iBossIndex] = INVALID_HANDLE; + g_hSlenderEntityThink[iBossIndex] = INVALID_HANDLE; + g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; + g_flSlenderNextTeleportTime[iBossIndex] = GetGameTime(); + g_flSlenderLastKill[iBossIndex] = GetGameTime(); + g_flSlenderTimeUntilKill[iBossIndex] = -1.0; + g_flSlenderNextJumpScare[iBossIndex] = -1.0; + g_flSlenderTimeUntilNextProxy[iBossIndex] = -1.0; + g_flSlenderTeleportMinRange[iBossIndex] = GetProfileFloat(sProfile, "teleport_range_min", 325.0); + g_flSlenderTeleportMaxRange[iBossIndex] = GetProfileFloat(sProfile, "teleport_range_max", 1024.0); + g_flSlenderStaticRadius[iBossIndex] = GetProfileFloat(sProfile, "static_radius"); + g_flSlenderIdleAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_idle_playbackrate", 1.0); + g_flSlenderWalkAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_walk_playbackrate", 1.0); + g_flSlenderRunAnimationPlaybackRate[iBossIndex] = GetProfileFloat(sProfile, "animation_run_playbackrate", 1.0); + g_flSlenderJumpSpeed[iBossIndex] = GetProfileFloat(sProfile, "jump_speed", 512.0); + g_flSlenderPathNodeTolerance[iBossIndex] = GetProfileFloat(sProfile, "search_node_dist_tolerance", 32.0); + g_flSlenderPathNodeLookAhead[iBossIndex] = GetProfileFloat(sProfile, "search_node_dist_lookahead", 512.0); + g_flSlenderProxyTeleportMinRange[iBossIndex] = GetProfileFloat(sProfile, "proxies_teleport_range_min"); + g_flSlenderProxyTeleportMaxRange[iBossIndex] = GetProfileFloat(sProfile, "proxies_teleport_range_max"); + + for (new i = 1; i <= MaxClients; i++) + { + g_flPlayerLastChaseBossEncounterTime[i][iBossIndex] = -1.0; + g_flSlenderTeleportPlayersRestTime[iBossIndex][i] = -1.0; + } + + g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; + g_flSlenderNextTeleportTime[iBossIndex] = -1.0; + g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; + + g_hSlenderThink[iBossIndex] = CreateTimer(0.1, Timer_SlenderTeleportThink, iBossIndex, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + SlenderRemoveTargetMemory(iBossIndex); + + switch (iBossType) + { + case SF2BossType_Chaser: + { + NPCChaserOnSelectProfile(iBossIndex); + + SlenderCreateTargetMemory(iBossIndex); + } + } + + if (iCopyMaster >= 0 && iCopyMaster < MAX_BOSSES && NPCGetUniqueID(iCopyMaster) != -1) + { + g_iSlenderCopyMaster[iBossIndex] = iCopyMaster; + g_flSlenderNextJumpScare[iBossIndex] = g_flSlenderNextJumpScare[iCopyMaster]; + + NPCSetAnger(iBossIndex, NPCGetAnger(iCopyMaster)); + } + else + { + if (bPlaySpawnSound) + { + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_spawn_all", sBuffer, sizeof(sBuffer)); + if (sBuffer[0]) EmitSoundToAll(sBuffer, _, SNDCHAN_STATIC, SNDLEVEL_HELICOPTER); + } + + if (bSpawnCompanions) + { + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + decl String:sCompProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + new Handle:hCompanions = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); + + if (KvJumpToKey(g_hConfig, "companions")) + { + decl String:sNum[32]; + + for (new i = 1;;i++) + { + IntToString(i, sNum, sizeof(sNum)); + KvGetString(g_hConfig, sNum, sCompProfile, sizeof(sCompProfile)); + if (!sCompProfile[0]) break; + + PushArrayString(hCompanions, sCompProfile); + } + } + + for (new i = 0, iSize = GetArraySize(hCompanions); i < iSize; i++) + { + GetArrayString(hCompanions, i, sCompProfile, sizeof(sCompProfile)); + AddProfile(sCompProfile, _, _, false, false); + } + + CloseHandle(hCompanions); + } + } + + Call_StartForward(fOnBossAdded); + Call_PushCell(iBossIndex); + Call_Finish(); + + return true; +} + +AddProfile(const String:strName[], iAdditionalBossFlags=0, iCopyMaster=-1, bool:bSpawnCompanions=true, bool:bPlaySpawnSound=true) +{ + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) + { + if (SelectProfile(i, strName, iAdditionalBossFlags, iCopyMaster, bSpawnCompanions, bPlaySpawnSound)) + { + return i; + } + + break; + } + } + + return -1; +} + +RemoveProfile(iBossIndex) +{ + RemoveSlender(iBossIndex); + + // Call our forward. + Call_StartForward(fOnBossRemoved); + Call_PushCell(iBossIndex); + Call_Finish(); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + NPCChaserOnRemoveProfile(iBossIndex); + + // Remove all possible sounds, for emergencies. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + // Remove chase music. + if (g_iPlayerChaseMusicMaster[i] == iBossIndex) + { + ClientStopAllSlenderSounds(i, sProfile, "sound_chase", SNDCHAN_AUTO); + } + } + + // Clean up on the clients. + for (new i = 1; i <= MaxClients; i++) + { + g_flSlenderLastFoundPlayer[iBossIndex][i] = -1.0; + g_flPlayerLastChaseBossEncounterTime[i][iBossIndex] = -1.0; + g_flSlenderTeleportPlayersRestTime[iBossIndex][i] = -1.0; + + for (new i2 = 0; i2 < 3; i2++) + { + g_flSlenderLastFoundPlayerPos[iBossIndex][i][i2] = 0.0; + } + + if (IsClientInGame(i)) + { + if (NPCGetUniqueID(iBossIndex) == g_iPlayerStaticMaster[i]) + { + g_iPlayerStaticMaster[i] = -1; + + // No one is the static master. + g_hPlayerStaticTimer[i] = CreateTimer(g_flPlayerStaticDecreaseRate[i], + Timer_ClientDecreaseStatic, + GetClientUserId(i), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerStaticTimer[i], true); + } + } + } + + g_iNPCTeleportType[iBossIndex] = -1; + g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; + g_flSlenderNextTeleportTime[iBossIndex] = -1.0; + g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; + g_flSlenderTimeUntilKill[iBossIndex] = -1.0; + + // Remove all copies associated with me. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (i == iBossIndex || NPCGetUniqueID(i) == -1) continue; + + if (g_iSlenderCopyMaster[i] == iBossIndex) + { + LogMessage("Removed boss index %d because it is a copy of boss index %d", i, iBossIndex); + NPCRemove(i); + } + } + + NPCSetProfile(iBossIndex, ""); + g_iNPCType[iBossIndex] = -1; + g_iNPCProfileIndex[iBossIndex] = -1; + g_iNPCUniqueProfileIndex[iBossIndex] = -1; + + NPCSetFlags(iBossIndex, 0); + + NPCSetAnger(iBossIndex, 1.0); + + g_flNPCFieldOfView[iBossIndex] = 0.0; + + g_iNPCEnemy[iBossIndex] = INVALID_ENT_REFERENCE; + + NPCSetDeathCamEnabled(iBossIndex, false); + + g_iSlenderCopyMaster[iBossIndex] = -1; + g_iNPCUniqueID[iBossIndex] = -1; + g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; + g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; + g_hSlenderThink[iBossIndex] = INVALID_HANDLE; + g_hSlenderEntityThink[iBossIndex] = INVALID_HANDLE; + + g_hSlenderFakeTimer[iBossIndex] = INVALID_HANDLE; + g_flSlenderLastKill[iBossIndex] = -1.0; + g_iSlenderState[iBossIndex] = STATE_IDLE; + g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_iSlenderModel[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderAcceleration[iBossIndex] = 0.0; + g_flSlenderTimeUntilNextProxy[iBossIndex] = -1.0; + g_flNPCSearchRadius[iBossIndex] = 0.0; + g_flNPCInstantKillRadius[iBossIndex] = 0.0; + g_flNPCScareRadius[iBossIndex] = 0.0; + g_flSlenderProxyTeleportMinRange[iBossIndex] = 0.0; + g_flSlenderProxyTeleportMaxRange[iBossIndex] = 0.0; + + for (new i = 0; i < 3; i++) + { + g_flSlenderDetectMins[iBossIndex][i] = 0.0; + g_flSlenderDetectMaxs[iBossIndex][i] = 0.0; + g_flSlenderEyePosOffset[iBossIndex][i] = 0.0; + } + + SlenderRemoveTargetMemory(iBossIndex); +} + +SpawnSlender(iBossIndex, const Float:pos[3]) +{ + RemoveSlender(iBossIndex); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl Float:flTruePos[3]; + GetProfileVector(sProfile, "pos_offset", flTruePos); + AddVectors(flTruePos, pos, flTruePos); + + new iSlenderModel = SpawnSlenderModel(iBossIndex, flTruePos); + if (iSlenderModel == -1) + { + LogError("Could not spawn boss: model failed to spawn!"); + return; + } + + decl String:sBuffer[PLATFORM_MAX_PATH]; + + g_iSlenderModel[iBossIndex] = EntIndexToEntRef(iSlenderModel); + + switch (NPCGetType(iBossIndex)) + { + case SF2BossType_Creeper: + { + g_iSlender[iBossIndex] = g_iSlenderModel[iBossIndex]; + g_hSlenderEntityThink[iBossIndex] = CreateTimer(BOSS_THINKRATE, Timer_SlenderBlinkBossThink, g_iSlender[iBossIndex], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + case SF2BossType_Chaser: + { + GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); + + new iBoss = CreateEntityByName("monster_generic"); + SetEntityModel(iBoss, sBuffer); + TeleportEntity(iBoss, flTruePos, NULL_VECTOR, NULL_VECTOR); + DispatchSpawn(iBoss); + ActivateEntity(iBoss); + SetEntityRenderMode(iBoss, RENDER_TRANSCOLOR); + SetEntityRenderColor(iBoss, 0, 0, 0, 1); + SetVariantString("!activator"); + AcceptEntityInput(iSlenderModel, "SetParent", iBoss); + AcceptEntityInput(iSlenderModel, "EnableShadow"); + SetEntProp(iSlenderModel, Prop_Send, "m_usSolidFlags", FSOLID_NOT_SOLID | FSOLID_TRIGGER); + AcceptEntityInput(iBoss, "DisableShadow"); + SetEntPropFloat(iBoss, Prop_Data, "m_flFriction", 0.0); + + NPCChaserSetStunHealth(iBossIndex, NPCChaserGetStunInitialHealth(iBossIndex)); + + // Reset stats. + g_iSlender[iBossIndex] = EntIndexToEntRef(iBoss); + g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_iSlenderState[iBossIndex] = STATE_IDLE; + g_bSlenderAttacking[iBossIndex] = false; + g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; + g_flSlenderTargetSoundLastTime[iBossIndex] = -1.0; + g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = -1.0; + g_iSlenderTargetSoundType[iBossIndex] = SoundType_None; + g_bSlenderInvestigatingSound[iBossIndex] = false; + g_flSlenderLastHeardFootstep[iBossIndex] = GetGameTime(); + g_flSlenderLastHeardVoice[iBossIndex] = GetGameTime(); + g_flSlenderLastHeardWeapon[iBossIndex] = GetGameTime(); + g_flSlenderNextVoiceSound[iBossIndex] = GetGameTime(); + g_flSlenderNextMoanSound[iBossIndex] = GetGameTime(); + g_flSlenderNextWanderPos[iBossIndex] = GetGameTime() + 3.0; + g_flSlenderTimeUntilKill[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "idle_lifetime", 10.0); + g_flSlenderTimeUntilRecover[iBossIndex] = -1.0; + g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; + g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; + g_flSlenderTimeUntilChase[iBossIndex] = -1.0; + g_flSlenderTimeUntilNoPersistence[iBossIndex] = -1.0; + g_flSlenderNextJump[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "jump_cooldown", 2.0); + g_flSlenderNextPathTime[iBossIndex] = GetGameTime(); + g_hSlenderEntityThink[iBossIndex] = CreateTimer(BOSS_THINKRATE, Timer_SlenderChaseBossThink, EntIndexToEntRef(iBoss), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_iSlenderInterruptConditions[iBossIndex] = 0; + g_bSlenderChaseDeathPosition[iBossIndex] = false; + + for (new i = 0; i < 3; i++) + { + g_flSlenderGoalPos[iBossIndex][i] = 0.0; + g_flSlenderTargetSoundTempPos[iBossIndex][i] = 0.0; + g_flSlenderTargetSoundMasterPos[iBossIndex][i] = 0.0; + g_flSlenderChaseDeathPosition[iBossIndex][i] = 0.0; + } + + for (new i = 1; i <= MaxClients; i++) + { + g_flSlenderLastFoundPlayer[iBossIndex][i] = -1.0; + + for (new i2 = 0; i2 < 3; i2++) + { + g_flSlenderLastFoundPlayerPos[iBossIndex][i][i2] = 0.0; + } + } + + SlenderClearTargetMemory(iBossIndex); + + if (GetProfileNum(sProfile, "stun_enabled")) + { + SetEntProp(iBoss, Prop_Data, "m_takedamage", 1); + } + + SDKHook(iBoss, SDKHook_OnTakeDamage, Hook_SlenderOnTakeDamage); + SDKHook(iBoss, SDKHook_OnTakeDamagePost, Hook_SlenderOnTakeDamagePost); + DHookEntity(g_hSDKShouldTransmit, true, iBoss); + } + /* + default: + { + g_iSlender[iBossIndex] = g_iSlenderModel[iBossIndex]; + SDKHook(iSlenderModel, SDKHook_SetTransmit, Hook_SlenderSetTransmit); + } + */ + } + + SDKHook(iSlenderModel, SDKHook_SetTransmit, Hook_SlenderModelSetTransmit); + + SlenderSpawnEffects(iBossIndex, EffectEvent_Constant); + + // Initialize our pose parameters, if needed. + new iPose = EntRefToEntIndex(g_iSlenderPoseEnt[iBossIndex]); + g_iSlenderPoseEnt[iBossIndex] = INVALID_ENT_REFERENCE; + if (iPose && iPose != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(iPose, "Kill"); + } + + decl String:sPoseParameter[64]; + GetProfileString(sProfile, "pose_parameter", sPoseParameter, sizeof(sPoseParameter)); + if (sPoseParameter[0]) + { + iPose = CreateEntityByName("point_posecontroller"); + if (iPose != -1) + { + // We got a pose parameter! We need a name! + Format(sBuffer, sizeof(sBuffer), "s%dposepls", g_iSlenderModel[iBossIndex]); + DispatchKeyValue(iSlenderModel, "targetname", sBuffer); + + DispatchKeyValue(iPose, "PropName", sBuffer); + DispatchKeyValue(iPose, "PoseParameterName", sPoseParameter); + DispatchKeyValueFloat(iPose, "PoseValue", GetProfileFloat(sProfile, "pose_parameter_max")); + DispatchSpawn(iPose); + SetVariantString(sPoseParameter); + AcceptEntityInput(iPose, "SetPoseParameterName"); + SetVariantString("!activator"); + AcceptEntityInput(iPose, "SetParent", iSlenderModel); + + g_iSlenderPoseEnt[iBossIndex] = EntIndexToEntRef(iPose); + } + } + + // Call our forward. + Call_StartForward(fOnBossSpawn); + Call_PushCell(iBossIndex); + Call_Finish(); +} + +RemoveSlender(iBossIndex) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iBoss = NPCGetEntIndex(iBossIndex); + g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; + + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + // Stop all possible looping sounds. + ClientStopAllSlenderSounds(iBoss, sProfile, "sound_move", SNDCHAN_AUTO); + + if (NPCGetFlags(iBossIndex) & SFF_HASSTATICLOOPLOCALSOUND) + { + decl String:sLoopSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); + + if (sLoopSound[0]) + { + StopSound(iBoss, SNDCHAN_STATIC, sLoopSound); + } + } + + AcceptEntityInput(iBoss, "Kill"); + } +} + +public Action:Hook_SlenderOnTakeDamage(slender, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return Plugin_Continue; + + if (NPCGetType(iBossIndex) == SF2BossType_Chaser) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (NPCChaserIsStunEnabled(iBossIndex)) + { + if (damagetype & DMG_ACID) damage *= 2.0; // 2x damage for critical hits. + + NPCChaserAddStunHealth(iBossIndex, -damage); + } + } + + damage = 0.0; + return Plugin_Changed; +} + +public Hook_SlenderOnTakeDamagePost(slender, attacker, inflictor, Float:damage, damagetype, weapon, const Float:damageForce[3], const Float:damagePosition[3]) +{ + if (!g_bEnabled) return; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return; + + if (NPCGetType(iBossIndex) == SF2BossType_Chaser) + { + if (damagetype & DMG_ACID) + { + decl Float:flMyEyePos[3]; + SlenderGetEyePosition(iBossIndex, flMyEyePos); + + TE_SetupTFParticleEffect(g_iParticleCriticalHit, flMyEyePos, flMyEyePos); + TE_SendToAll(); + + EmitSoundToAll(CRIT_SOUND, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + } + } +} + +public Action:Hook_SlenderModelSetTransmit(entity, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iBossIndex = -1; + + new entref = EntIndexToEntRef(entity); + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + if (g_iSlenderModel[i] != entref) continue; + + iBossIndex = i; + break; + } + + if (iBossIndex == -1) return Plugin_Continue; + + if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) return Plugin_Handled; + return Plugin_Continue; +} + +stock bool:SlenderCanHearPlayer(iBossIndex, client, SoundType:iSoundType) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) return false; + + new iSlender = NPCGetEntIndex(iBossIndex); + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) return false; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl Float:flHisPos[3], Float:flMyPos[3]; + GetClientAbsOrigin(client, flHisPos); + SlenderGetAbsOrigin(iBossIndex, flMyPos); + + new Float:flHearRadius = GetProfileFloat(sProfile, "search_sound_range", 1024.0); + if (flHearRadius <= 0.0) return false; + + new Float:flDistance = GetVectorDistance(flHisPos, flMyPos); + + // Trace check. + new Handle:hTrace = INVALID_HANDLE; + new bool:bTraceHit = false; + + decl Float:flMyEyePos[3]; + SlenderGetEyePosition(iBossIndex, flMyEyePos); + + if (iSoundType == SoundType_Footstep) + { + if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; + + if (GetEntProp(client, Prop_Send, "m_bDucking") || GetEntProp(client, Prop_Send, "m_bDucked")) flDistance *= 1.85; + if (IsClientReallySprinting(client)) flDistance *= 0.66; + + hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); + bTraceHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + } + else if (iSoundType == SoundType_Voice) + { + decl Float:flHisEyePos[3]; + GetClientEyePosition(client, flHisEyePos); + + hTrace = TR_TraceRayFilterEx(flMyEyePos, flHisEyePos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); + bTraceHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + flDistance *= 0.5; + } + else if (iSoundType == SoundType_Weapon) + { + decl Float:flHisMins[3], Float:flHisMaxs[3]; + GetEntPropVector(client, Prop_Send, "m_vecMins", flHisMins); + GetEntPropVector(client, Prop_Send, "m_vecMaxs", flHisMaxs); + + new Float:flMiddle[3]; + for (new i = 0; i < 2; i++) flMiddle[i] = (flHisMins[i] + flHisMaxs[i]) / 2.0; + + decl Float:flEndPos[3]; + GetClientAbsOrigin(client, flEndPos); + AddVectors(flHisPos, flMiddle, flEndPos); + + hTrace = TR_TraceRayFilterEx(flMyEyePos, flEndPos, MASK_NPCSOLID, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, iSlender); + bTraceHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + flDistance *= 0.66; + } + + if (bTraceHit) flDistance *= 1.66; + + if (TF2_GetPlayerClass(client) == TFClass_Spy) flDistance *= 1.35; + + if (flDistance > flHearRadius) return false; + + return true; +} + +stock SlenderArrayIndexToEntIndex(iBossIndex) +{ + return NPCGetEntIndex(iBossIndex); +} + +stock bool:SlenderOnlyLooksIfNotSeen(iBossIndex) +{ + if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return true; + return false; +} + +stock bool:SlenderUsesBlink(iBossIndex) +{ + if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return true; + return false; +} + +stock bool:SlenderKillsOnNear(iBossIndex) +{ + if (NPCGetType(iBossIndex) == SF2BossType_Creeper) return false; + return true; +} + +stock SlenderClearTargetMemory(iBossIndex) +{ + if (iBossIndex == -1) return; + + g_iSlenderCurrentPathNode[iBossIndex] = -1; + if (g_hSlenderPath[iBossIndex] == INVALID_HANDLE) return; + + ClearArray(g_hSlenderPath[iBossIndex]); +} + +stock bool:SlenderCreateTargetMemory(iBossIndex) +{ + if (iBossIndex == -1) return false; + + g_iSlenderCurrentPathNode[iBossIndex] = -1; + if (g_hSlenderPath[iBossIndex] != INVALID_HANDLE) return true; + + g_hSlenderPath[iBossIndex] = CreateArray(3); + return true; +} + +stock SlenderRemoveTargetMemory(iBossIndex) +{ + if (iBossIndex == -1) return; + + g_iSlenderCurrentPathNode[iBossIndex] = -1; + + if (g_hSlenderPath[iBossIndex] == INVALID_HANDLE) return; + + new Handle:hLocs = g_hSlenderPath[iBossIndex]; + g_hSlenderPath[iBossIndex] = INVALID_HANDLE; + CloseHandle(hLocs); +} + +SlenderPerformVoice(iBossIndex, const String:sSectionName[], iIndex=-1) +{ + if (iBossIndex == -1) return; + + new slender = NPCGetEntIndex(iBossIndex); + if (!slender || slender == INVALID_ENT_REFERENCE) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sPath[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, sSectionName, sPath, sizeof(sPath), iIndex); + if (sPath[0]) + { + decl String:sBuffer[512]; + strcopy(sBuffer, sizeof(sBuffer), sSectionName); + StrCat(sBuffer, sizeof(sBuffer), "_cooldown_min"); + new Float:flCooldownMin = GetProfileFloat(sProfile, sBuffer, 1.5); + strcopy(sBuffer, sizeof(sBuffer), sSectionName); + StrCat(sBuffer, sizeof(sBuffer), "_cooldown_max"); + new Float:flCooldownMax = GetProfileFloat(sProfile, sBuffer, 1.5); + new Float:flCooldown = GetRandomFloat(flCooldownMin, flCooldownMax); + strcopy(sBuffer, sizeof(sBuffer), sSectionName); + StrCat(sBuffer, sizeof(sBuffer), "_volume"); + new Float:flVolume = GetProfileFloat(sProfile, sBuffer, 1.0); + strcopy(sBuffer, sizeof(sBuffer), sSectionName); + StrCat(sBuffer, sizeof(sBuffer), "_channel"); + new iChannel = GetProfileNum(sProfile, sBuffer, SNDCHAN_AUTO); + strcopy(sBuffer, sizeof(sBuffer), sSectionName); + StrCat(sBuffer, sizeof(sBuffer), "_level"); + new iLevel = GetProfileNum(sProfile, sBuffer, SNDLEVEL_SCREAMING); + + g_flSlenderNextVoiceSound[iBossIndex] = GetGameTime() + flCooldown; + EmitSoundToAll(sPath, slender, iChannel, iLevel, _, flVolume); + } +} + +bool:SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, Float:buffer[3]) +{ + if (!IsValidClient(iBestPlayer)) return false; + + new slender = NPCGetEntIndex(iBossIndex); + if (!slender || slender == INVALID_ENT_REFERENCE) return false; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl Float:flSlenderPos[3], Float:flPos[3], Float:flReferenceAng[3], Float:hisEyeAng[3], Float:tempDir[3], Float:tempPos[3]; + GetClientEyePosition(iBestPlayer, flPos); + + GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", hisEyeAng); + AddVectors(hisEyeAng, g_flSlenderEyeAngOffset[iBossIndex], hisEyeAng); + for (new i = 0; i < 3; i++) hisEyeAng[i] = AngleNormalize(hisEyeAng[i]); + + SlenderGetAbsOrigin(iBossIndex, flSlenderPos); + + SubtractVectors(flPos, flSlenderPos, flReferenceAng); + GetVectorAngles(flReferenceAng, flReferenceAng); + for (new i = 0; i < 3; i++) flReferenceAng[i] = AngleNormalize(flReferenceAng[i]); + new Float:flDist = GetProfileFloat(sProfile, "speed") * g_flRoundDifficultyModifier; + if (flDist < GetProfileFloat(sProfile, "kill_radius")) flDist = GetProfileFloat(sProfile, "kill_radius") / 2.0; + new Float:flWithinFOV = 45.0; + new Float:flWithinFOVSide = 90.0; + + decl Handle:hTrace, index, Float:flHitNormal[3], Float:tempPos2[3], Float:flBuffer[3], Float:flBuffer2[3]; + new Handle:hArray = CreateArray(6); + + decl Float:flCheckAng[3]; + + new iRange = 0; + new iID = 1; + + for (new Float:addAng = 0.0; addAng < 360.0; addAng += 7.5) + { + tempDir[0] = 0.0; + tempDir[1] = AngleNormalize(hisEyeAng[1] + addAng); + tempDir[2] = 0.0; + + GetAngleVectors(tempDir, tempDir, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(tempDir, tempDir); + ScaleVector(tempDir, flDist); + AddVectors(tempDir, flSlenderPos, tempPos); + AddVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); + AddVectors(flSlenderPos, g_flSlenderEyePosOffset[iBossIndex], tempPos2); + + flBuffer[0] = g_flSlenderDetectMins[iBossIndex][0]; + flBuffer[1] = g_flSlenderDetectMins[iBossIndex][1]; + flBuffer[2] = 0.0; + flBuffer2[0] = g_flSlenderDetectMaxs[iBossIndex][0]; + flBuffer2[1] = g_flSlenderDetectMaxs[iBossIndex][1]; + flBuffer2[2] = 0.0; + + // Get a good move position. + hTrace = TR_TraceHullFilterEx(tempPos2, tempPos, flBuffer, flBuffer2, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitCharactersOrEntity, slender); + TR_GetEndPosition(tempPos, hTrace); + CloseHandle(hTrace); + + // Drop to the ground if we're above ground. + hTrace = TR_TraceRayFilterEx(tempPos, Float:{ 90.0, 0.0, 0.0 }, MASK_PLAYERSOLID_BRUSHONLY, RayType_Infinite, TraceRayDontHitCharactersOrEntity, slender); + new bool:bHit = TR_DidHit(hTrace); + TR_GetEndPosition(tempPos2, hTrace); + CloseHandle(hTrace); + + // Then calculate from there. + hTrace = TR_TraceHullFilterEx(tempPos, tempPos2, g_flSlenderDetectMins[iBossIndex], g_flSlenderDetectMaxs[iBossIndex], MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitCharactersOrEntity, slender); + TR_GetEndPosition(tempPos, hTrace); + TR_GetPlaneNormal(hTrace, flHitNormal); + CloseHandle(hTrace); + SubtractVectors(tempPos, flSlenderPos, flCheckAng); + GetVectorAngles(flCheckAng, flCheckAng); + GetVectorAngles(flHitNormal, flHitNormal); + for (new i = 0; i < 3; i++) + { + flHitNormal[i] = AngleNormalize(flHitNormal[i]); + flCheckAng[i] = AngleNormalize(flCheckAng[i]); + } + + new Float:diff = AngleDiff(flCheckAng[1], flReferenceAng[1]); + + new bool:bBackup = false; + + if (FloatAbs(diff) > flWithinFOV) bBackup = true; + + if (diff >= 0.0 && diff <= flWithinFOVSide) iRange = 1; + else if (diff < 0.0 && diff >= -flWithinFOVSide) iRange = 2; + else continue; + + if ((flHitNormal[0] >= 0.0 && flHitNormal[0] < 45.0) + || (flHitNormal[0] < 0.0 && flHitNormal[0] > -45.0) + || !bHit + || TR_PointOutsideWorld(tempPos) + || IsSpaceOccupiedNPC(tempPos, g_flSlenderDetectMins[iBossIndex], g_flSlenderDetectMaxs[iBossIndex], iBestPlayer)) + { + continue; + } + + // Check from top to bottom of me. + + if (!IsPointVisibleToPlayer(iBestPlayer, tempPos, false, false)) continue; + + AddVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); + + if (!IsPointVisibleToPlayer(iBestPlayer, tempPos, false, false)) continue; + + SubtractVectors(tempPos, g_flSlenderEyePosOffset[iBossIndex], tempPos); + + // Insert the vector into our array. + index = PushArrayCell(hArray, iID); + SetArrayCell(hArray, index, tempPos[0], 1); + SetArrayCell(hArray, index, tempPos[1], 2); + SetArrayCell(hArray, index, tempPos[2], 3); + SetArrayCell(hArray, index, iRange, 4); + SetArrayCell(hArray, index, bBackup, 5); + + iID++; + } + + new size; + if ((size = GetArraySize(hArray)) > 0) + { + new Float:diff = AngleDiff(hisEyeAng[1], flReferenceAng[1]); + if (diff >= 0.0) iRange = 1; + else iRange = 2; + + new bool:bBackup = false; + + // Clean up any vectors that we don't need. + new Handle:hArray2 = CloneArray(hArray); + for (new i = 0; i < size; i++) + { + if (GetArrayCell(hArray2, i, 4) != iRange || bool:GetArrayCell(hArray2, i, 5) != bBackup) + { + new iIndex = FindValueInArray(hArray, GetArrayCell(hArray2, i)); + if (iIndex != -1) RemoveFromArray(hArray, iIndex); + } + } + + CloseHandle(hArray2); + + size = GetArraySize(hArray); + if (size) + { + index = GetRandomInt(0, size - 1); + buffer[0] = Float:GetArrayCell(hArray, index, 1); + buffer[1] = Float:GetArrayCell(hArray, index, 2); + buffer[2] = Float:GetArrayCell(hArray, index, 3); + } + else + { + CloseHandle(hArray); + return false; + } + } + else + { + CloseHandle(hArray); + return false; + } + + CloseHandle(hArray); + return true; +} + +// This functor ensures that the proposed boss position is not too +// close to other players that are within the distance defined by +// flMinSearchDist. + +// Returning false on the functor will immediately discard the proposed position. + +public bool:SlenderChaseBossPlaceFunctor(iBossIndex, const Float:flActiveAreaCenterPos[3], const Float:flAreaPos[3], Float:flMinSearchDist, Float:flMaxSearchDist, bool:bOriginalResult) +{ + if (FloatAbs(flActiveAreaCenterPos[2] - flAreaPos[2]) > 320.0) + { + return false; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + g_bPlayerEscaped[i]) continue; + + decl Float:flClientPos[3]; + GetClientAbsOrigin(i, flClientPos); + + if (GetVectorDistance(flClientPos, flAreaPos) < flMinSearchDist) + { + return false; + } + } + + return bOriginalResult; +} + +// As time passes on, we have to get more aggressive in order to successfully peak the target's +// stress level in the allotted duration we're given. Otherwise we'll be forced to place him +// in a rest period. + +// Teleport progressively closer as time passes in attempt to increase the target's stress level. +// Maximum minimum range is capped by the boss's anger level. + +stock Float:CalculateTeleportMinRange(iBossIndex, Float:flInitialMinRange, Float:flTeleportMaxRange) +{ + new Float:flTeleportTargetTimeLeft = g_flSlenderTeleportMaxTargetTime[iBossIndex] - GetGameTime(); + new Float:flTeleportTargetTimeInitial = g_flSlenderTeleportMaxTargetTime[iBossIndex] - g_flSlenderTeleportTargetTime[iBossIndex]; + new Float:flTeleportMinRange = flTeleportMaxRange - (1.0 - (flTeleportTargetTimeLeft / flTeleportTargetTimeInitial)) * (flTeleportMaxRange - flInitialMinRange); + + if (NPCGetAnger(iBossIndex) <= 1.0) + { + flTeleportMinRange += (g_flSlenderTeleportMinRange[iBossIndex] - flTeleportMaxRange) * Pow(NPCGetAnger(iBossIndex) - 1.0, 2.0 / g_flRoundDifficultyModifier); + } + + if (flTeleportMinRange < flInitialMinRange) flTeleportMinRange = flInitialMinRange; + if (flTeleportMinRange > flTeleportMaxRange) flTeleportMinRange = flTeleportMaxRange; + + return flTeleportMinRange; +} + +public Action:Timer_SlenderTeleportThink(Handle:timer, any:iBossIndex) +{ + if (iBossIndex == -1) return Plugin_Stop; + if (timer != g_hSlenderThink[iBossIndex]) return Plugin_Stop; + + if (NPCGetFlags(iBossIndex) & SFF_NOTELEPORT) return Plugin_Continue; + + // Check to see if anyone's looking at me before doing anything. + if (PeopleCanSeeSlender(iBossIndex, _, false)) + { + return Plugin_Continue; + } + + if (NPCGetTeleportType(iBossIndex) == 2) + { + new iBoss = NPCGetEntIndex(iBossIndex); + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + if (NPCGetType(iBossIndex) == SF2BossType_Chaser) + { + // Check to see if it's a good time to teleport away. + new iState = g_iSlenderState[iBossIndex]; + if (iState == STATE_IDLE || iState == STATE_WANDER) + { + if (GetGameTime() < g_flSlenderTimeUntilKill[iBossIndex]) + { + return Plugin_Continue; + } + } + } + } + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (!g_bRoundGrace) + { + if (GetGameTime() >= g_flSlenderNextTeleportTime[iBossIndex]) + { + new Float:flTeleportTime = GetRandomFloat(GetProfileFloat(sProfile, "teleport_time_min", 5.0), GetProfileFloat(sProfile, "teleport_time_max", 9.0)); + g_flSlenderNextTeleportTime[iBossIndex] = GetGameTime() + flTeleportTime; + + new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); + + if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) + { + // We don't have any good targets. Remove myself for now. + if (SlenderCanRemove(iBossIndex)) RemoveSlender(iBossIndex); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: no good target, removing...", iBossIndex); +#endif + } + else + { + new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderTeleportMinRange[iBossIndex], g_flSlenderTeleportMaxRange[iBossIndex]); + + new iTeleportAreaIndex = -1; + decl Float:flTeleportPos[3]; + + // Search surrounding nav areas around target. + if (NavMesh_Exists()) + { + decl Float:flTargetPos[3]; + GetClientAbsOrigin(iTeleportTarget, flTargetPos); + + new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); + if (iTargetAreaIndex != -1) + { + new bool:bShouldBeBehindObstruction = false; + if (NPCGetTeleportType(iBossIndex) == 2) + { + bShouldBeBehindObstruction = true; + } + + // Search outwards until travel distance is at maximum range. + new Handle:hAreaArray = CreateArray(2); + new Handle:hAreas = CreateStack(); + NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderTeleportMaxRange[iBossIndex]); + + { + new iPoppedAreas; + + while (!IsStackEmpty(hAreas)) + { + new iAreaIndex = -1; + PopStackCell(hAreas, iAreaIndex); + + // Check flags. + if (NavMeshArea_GetFlags(iAreaIndex) & NAV_MESH_NO_HOSTAGES) + { + // Don't spawn/teleport at areas marked with the "NO HOSTAGES" flag. + continue; + } + + new iIndex = PushArrayCell(hAreaArray, iAreaIndex); + SetArrayCell(hAreaArray, iIndex, float(NavMeshArea_GetCostSoFar(iAreaIndex)), 1); + iPoppedAreas++; + } + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: collected %d areas", iBossIndex, iPoppedAreas); +#endif + + CloseHandle(hAreas); + } + + new Handle:hAreaArrayClose = CreateArray(4); + new Handle:hAreaArrayAverage = CreateArray(4); + new Handle:hAreaArrayFar = CreateArray(4); + + for (new i = 1; i <= 3; i++) + { + new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(i - 1) / 3.0); + new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(i) / 3.0); + + for (new i2 = 0, iSize = GetArraySize(hAreaArray); i2 < iSize; i2++) + { + new iAreaIndex = GetArrayCell(hAreaArray, i2); + + decl Float:flAreaSpawnPoint[3]; + NavMeshArea_GetCenter(iAreaIndex, flAreaSpawnPoint); + + new iBoss = NPCGetEntIndex(iBossIndex); + + // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. + { + decl Float:flTraceStartPos[3]; + flTraceStartPos[0] = flAreaSpawnPoint[0]; + flTraceStartPos[1] = flAreaSpawnPoint[1]; + flTraceStartPos[2] = flAreaSpawnPoint[2] + (HalfHumanHeight * 2.0); + + decl Float:flTraceMins[3]; + flTraceMins[0] = g_flSlenderDetectMins[iBossIndex][0]; + flTraceMins[1] = g_flSlenderDetectMins[iBossIndex][1]; + flTraceMins[2] = 0.0; + + + decl Float:flTraceMaxs[3]; + flTraceMaxs[0] = g_flSlenderDetectMaxs[iBossIndex][0]; + flTraceMaxs[1] = g_flSlenderDetectMaxs[iBossIndex][1]; + flTraceMaxs[2] = 0.0; + + new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flAreaSpawnPoint, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + decl Float:flTraceHitPos[3]; + TR_GetEndPosition(flTraceHitPos, hTrace); + flTraceHitPos[2] += 1.0; + CloseHandle(hTrace); + + if (IsSpaceOccupiedNPC(flTraceHitPos, + g_flSlenderDetectMins[iBossIndex], + g_flSlenderDetectMaxs[iBossIndex], + iBoss)) + { + continue; + } + + if (NPCGetType(iBossIndex) == SF2BossType_Chaser) + { + if (IsSpaceOccupiedNPC(flTraceHitPos, + HULL_HUMAN_MINS, + HULL_HUMAN_MAXS, + iBoss)) + { + // Can't let an NPC spawn here; too little space. If we let it spawn here it will be non solid! + continue; + } + } + + flAreaSpawnPoint[0] = flTraceHitPos[0]; + flAreaSpawnPoint[1] = flTraceHitPos[1]; + flAreaSpawnPoint[2] = flTraceHitPos[2]; + } + + // Check visibility. + if (IsPointVisibleToAPlayer(flAreaSpawnPoint, !bShouldBeBehindObstruction, false)) continue; + + AddVectors(flAreaSpawnPoint, g_flSlenderEyePosOffset[iBossIndex], flAreaSpawnPoint); + + if (IsPointVisibleToAPlayer(flAreaSpawnPoint, !bShouldBeBehindObstruction, false)) continue; + + SubtractVectors(flAreaSpawnPoint, g_flSlenderEyePosOffset[iBossIndex], flAreaSpawnPoint); + + new bool:bTooNear = false; + + // Check minimum range with players. + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || + !IsPlayerAlive(iClient) || + g_bPlayerEliminated[iClient] || + IsClientInGhostMode(iClient) || + DidClientEscape(iClient)) + { + continue; + } + + decl Float:flTempPos[3]; + GetClientAbsOrigin(iClient, flTempPos); + + if (GetVectorDistance(flAreaSpawnPoint, flTempPos) <= g_flSlenderTeleportMinRange[iBossIndex]) + { + bTooNear = true; + break; + } + } + + if (bTooNear) continue; // This area is not compatible. + + // Check minimum range with boss copies (if supported). + if (NPCGetFlags(iBossIndex) & SFF_COPIES) + { + new Float:flMinDistBetweenBosses = GetProfileFloat(sProfile, "copy_teleport_dist_from_others", 800.0); + + for (new iBossCheck = 0; iBossCheck < MAX_BOSSES; iBossCheck++) + { + if (iBossCheck == iBossIndex || + NPCGetUniqueID(iBossCheck) == -1 || + (g_iSlenderCopyMaster[iBossIndex] != iBossCheck && g_iSlenderCopyMaster[iBossIndex] != g_iSlenderCopyMaster[iBossCheck])) + { + continue; + } + + new iBossEnt = NPCGetEntIndex(iBossCheck); + if (!iBossEnt || iBossEnt == INVALID_ENT_REFERENCE) continue; + + decl Float:flTempPos[3]; + SlenderGetAbsOrigin(iBossCheck, flTempPos); + + if (GetVectorDistance(flAreaSpawnPoint, flTempPos) <= flMinDistBetweenBosses) + { + bTooNear = true; + break; + } + } + } + + if (bTooNear) continue; // This area is not compatible. + + // Check travel distance and put in the appropriate arrays. + new Float:flDist = Float:GetArrayCell(hAreaArray, i2, 1); + if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) + { + new iIndex = -1; + new Handle:hTargetAreaArray = INVALID_HANDLE; + + switch (i) + { + case 1: + { + iIndex = PushArrayCell(hAreaArrayClose, iAreaIndex); + hTargetAreaArray = hAreaArrayClose; + } + case 2: + { + iIndex = PushArrayCell(hAreaArrayAverage, iAreaIndex); + hTargetAreaArray = hAreaArrayAverage; + } + case 3: + { + iIndex = PushArrayCell(hAreaArrayFar, iAreaIndex); + hTargetAreaArray = hAreaArrayFar; + } + } + + if (hTargetAreaArray != INVALID_HANDLE && iIndex != -1) + { + SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[0], 1); + SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[1], 2); + SetArrayCell(hTargetAreaArray, iIndex, flAreaSpawnPoint[2], 3); + } + } + } + } + + CloseHandle(hAreaArray); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: collected %d close areas, %d average areas, %d far areas", iBossIndex, GetArraySize(hAreaArrayClose), + GetArraySize(hAreaArrayAverage), + GetArraySize(hAreaArrayFar)); +#endif + + new iArrayIndex = -1; + + if (GetArraySize(hAreaArrayClose)) + { + iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1); + iTeleportAreaIndex = GetArrayCell(hAreaArrayClose, iArrayIndex); + flTeleportPos[0] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 1); + flTeleportPos[1] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 2); + flTeleportPos[2] = Float:GetArrayCell(hAreaArrayClose, iArrayIndex, 3); + } + else if (GetArraySize(hAreaArrayAverage)) + { + iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1); + iTeleportAreaIndex = GetArrayCell(hAreaArrayAverage, iArrayIndex); + flTeleportPos[0] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 1); + flTeleportPos[1] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 2); + flTeleportPos[2] = Float:GetArrayCell(hAreaArrayAverage, iArrayIndex, 3); + } + else if (GetArraySize(hAreaArrayFar)) + { + iArrayIndex = GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1); + iTeleportAreaIndex = GetArrayCell(hAreaArrayFar, iArrayIndex); + flTeleportPos[0] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 1); + flTeleportPos[1] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 2); + flTeleportPos[2] = Float:GetArrayCell(hAreaArrayFar, iArrayIndex, 3); + } + + CloseHandle(hAreaArrayClose); + CloseHandle(hAreaArrayAverage); + CloseHandle(hAreaArrayFar); + } + else + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because target is not on nav mesh!", iBossIndex); +#endif + } + } + else + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because of lack of nav mesh!", iBossIndex); +#endif + } + + if (iTeleportAreaIndex == -1) + { + // We don't have any good areas. Remove myself for now. + if (SlenderCanRemove(iBossIndex)) RemoveSlender(iBossIndex); + } + else + { + SpawnSlender(iBossIndex, flTeleportPos); + + if (NPCGetFlags(iBossIndex) & SFF_HASJUMPSCARE) + { + new bool:bDidJumpScare = false; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; + + if (PlayerCanSeeSlender(i, iBossIndex, false)) + { + if ((NPCGetDistanceFromEntity(iBossIndex, i) <= GetProfileFloat(sProfile, "jumpscare_distance") && + GetGameTime() >= g_flSlenderNextJumpScare[iBossIndex]) || + PlayerCanSeeSlender(i, iBossIndex)) + { + bDidJumpScare = true; + + new Float:flJumpScareDuration = GetProfileFloat(sProfile, "jumpscare_duration"); + ClientDoJumpScare(i, iBossIndex, flJumpScareDuration); + } + } + } + + if (bDidJumpScare) + { + g_flSlenderNextJumpScare[iBossIndex] = GetGameTime() + GetProfileFloat(sProfile, "jumpscare_cooldown"); + } + } + } + } + } + else + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: failed because of teleport time (curtime: %f, teletime: %f)", iBossIndex, GetGameTime(), g_flSlenderNextTeleportTime[iBossIndex]); +#endif + } + } + + return Plugin_Continue; +} + +/* +// Deprecated. + +// This is just to calculate the new place, not do time checks. +// Distance will be determined by the progression of the game and the +// manually set values determined by flMinSearchDist and flMaxSearchDist, +// which are float values that are (or should be) defined in the boss's +// config file. + +// The place chosen should be out of (possible) sight of the players, +// but should be within the AAS radius, the center being flActiveAreaCenterPos. +// The game will try to find a place that is of flMinSearchDist first, but +// if it can't, then it will try to find places that are a bit farther. + +// If the whole function fails, no place is given and the boss will not +// be able to spawn. + +bool:SlenderChaseBossCalculateNewPlace(iBossIndex, const Float:flActiveAreaCenterPos[3], Float:flMinSearchDist, Float:flMaxSearchDist, Function:iFunctor, Float:flBuffer[3]) +{ + new Handle:hAreas = NavMesh_GetAreas(); + if (hAreas == INVALID_HANDLE) return false; + + new iBestAreaIndex = -1; + new Float:flBestAreaDist = -1.0; + + decl Float:flAreaCenterPos[3]; + for (new i = 0, iSize = GetArraySize(hAreas); i < iSize; i++) + { + NavMeshArea_GetCenter(i, flAreaCenterPos); + + new Float:flDist = GetVectorDistance(flActiveAreaCenterPos, flAreaCenterPos); + if (flDist < flMinSearchDist || flDist > flMaxSearchDist) continue; + + if (IsPointVisibleToAPlayer(flAreaCenterPos, false, false)) continue; + + decl Float:flTestPos[3]; + for (new i2 = 0; i2 < 3; i2++) flTestPos[i2] = flAreaCenterPos[i2] + g_flSlenderEyePosOffset[iBossIndex][i2]; + + if (IsPointVisibleToAPlayer(flTestPos, false, false)) continue; + + if (iFunctor != INVALID_FUNCTION) + { + new bool:bResult = true; + + Call_StartFunction(INVALID_HANDLE, iFunctor); + Call_PushCell(iBossIndex); + Call_PushArray(flActiveAreaCenterPos, 3); + Call_PushArray(flAreaCenterPos, 3); + Call_PushFloat(flMinSearchDist); + Call_PushFloat(flMaxSearchDist); + Call_PushCell(bResult); + Call_Finish(bResult); + + if (!bResult) continue; + } + + if (flBestAreaDist < 0.0 || flDist < flBestAreaDist) + { + iBestAreaIndex = i; + flBestAreaDist = flDist; + } + } + + if (iBestAreaIndex == -1) return false; + + NavMeshArea_GetCenter(iBestAreaIndex, flBuffer); + return true; +} +*/ + +bool:SlenderCalculateNewPlace(iBossIndex, Float:buffer[3], bool:bIgnoreCopies=false, bool:bProxy=false, iProxyPlayer=-1, &iBestPlayer=-1) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new Float:flPercent = 0.0; + if (g_iPageMax > 0) + { + flPercent = (float(g_iPageCount) / float(g_iPageMax)) * g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex); + } + +#if defined DEBUG + new iArraySize, iArraySize2; +#endif + + if (!IsValidClient(iBestPlayer)) + { + // Pick a player to appear to. + new Handle:hArray = CreateArray(); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || + !IsPlayerAlive(i) || + IsClientInDeathCam(i) || + g_bPlayerEliminated[i] || + g_bPlayerEscaped[i]) continue; + + if (NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]) != -1 && !bIgnoreCopies) + { + new bool:bwub = false; + + // No? Then check if players around him are targeted by a boss already (not me). + for (new iBossPlayer = 1; iBossPlayer <= MaxClients; iBossPlayer++) + { + if (i == iBossPlayer) continue; + + if (!IsClientInGame(iBossPlayer) || + !IsPlayerAlive(iBossPlayer) || + IsClientInDeathCam(iBossPlayer) || + g_bPlayerEliminated[iBossPlayer] || + g_bPlayerEscaped[iBossPlayer]) continue; + + // Get the boss that's targeting this player, if any. + for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) + { + if (iBossIndex == iBoss || NPCGetUniqueID(iBoss) == -1) continue; + + if (EntRefToEntIndex(g_iSlenderTarget[iBoss]) == iBossPlayer) + { + // Are we near this player? + if (EntityDistanceFromEntity(iBossPlayer, i) < SF2_BOSS_COPY_SPAWN_MIN_DISTANCE) + { + bwub = true; + break; + } + } + } + } + + if (bwub) continue; + } + + PushArrayCell(hArray, i); + } + +#if defined DEBUG + iArraySize = GetArraySize(hArray); + iArraySize2 = iArraySize; +#endif + + if (GetArraySize(hArray)) + { + if (g_iSlenderCopyMaster[iBossIndex] == -1 || + GetProfileNum(sProfile, "copy_calculatepagecount", 0)) + { + new tempBestPageCount = -1; + + new Handle:hTempArray = CloneArray(hArray); + for (new i = 0; i < GetArraySize(hTempArray); i++) + { + new iClient = GetArrayCell(hTempArray, i); + if (g_iPlayerPageCount[iClient] > tempBestPageCount) + { + tempBestPageCount = g_iPlayerPageCount[iClient]; + } + } + + for (new i = 0; i < GetArraySize(hTempArray); i++) + { + new iClient = GetArrayCell(hTempArray, i); + if ((float(g_iPlayerPageCount[iClient]) / float(tempBestPageCount)) < SF2_BOSS_PAGE_CALCULATION) + { + new index = FindValueInArray(hArray, iClient); + if (index != -1) RemoveFromArray(hArray, index); + } + } + + CloseHandle(hTempArray); + } + +#if defined DEBUG + iArraySize2 = GetArraySize(hArray); +#endif + } + + if (GetArraySize(hArray)) + { + iBestPlayer = GetArrayCell(hArray, GetRandomInt(0, GetArraySize(hArray) - 1)); + } + + CloseHandle(hArray); + } + +#if defined DEBUG + if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d): array size 1 = %d, array size 2 = %d", iBossIndex, iArraySize, iArraySize2); +#endif + + if (iBestPlayer <= 0) + { +#if defined DEBUG + if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d) failed: no ibestPlayer!", iBossIndex); +#endif + return false; + } + + // Determine the distance we can appear from the player. + new Float:flPercentFar = 0.75 * (1.0 - flPercent); + new Float:flPercentAverage = 0.6 * (1.0 - flPercent); + //new Float:flPercentClose = 1.0 - flPercentFar - flPercentAverage; + + new Float:flUpperBoundFar = flPercentFar; + new Float:flUpperBoundAverage = flPercentFar + flPercentAverage; + //new Float:flUpperBoundClose = 1.0; + + new iRange = 1; + new Float:flChance = GetRandomFloat(0.0, 1.0); + new Float:flMaxRangeN = GetProfileFloat(sProfile, "teleport_range_max"); + new Float:flMinRangeN = GetProfileFloat(sProfile, "teleport_range_min"); + + new bool:bVisiblePls = false; + new bool:bBeCreepy = false; + + if (!bProxy) + { + // Are we gonna teleport in front of a player this time? + if (GetProfileNum(sProfile, "teleport_ignorevis_enable")) + { + if (GetRandomFloat(0.0, 1.0) < GetProfileFloat(sProfile, "teleport_ignorevis_chance") * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier) + { + bVisiblePls = true; + } + + if (GetRandomFloat(0.0, 1.0) < GetProfileFloat(sProfile, "teleport_creepy_chance", 0.33)) + { + bBeCreepy = true; + } + } + } + + new Float:flMaxRange = flMaxRangeN; + new Float:flMinRange = flMinRangeN; + + if (bVisiblePls) + { + flMaxRange = GetProfileFloat(sProfile, "teleport_ignorevis_range_max", flMaxRangeN); + flMinRange = GetProfileFloat(sProfile, "teleport_ignorevis_range_min", flMinRangeN); + } + + // Get distances. + new Float:flDistanceFar = GetRandomFloat(flMaxRange * 0.75, flMaxRange); + if (flDistanceFar < flMinRange) flDistanceFar = flMinRange; + new Float:flDistanceAverage = GetRandomFloat(flMaxRange * 0.33, flMaxRange * 0.75); + if (flDistanceAverage < flMinRange) flDistanceAverage = flMinRange; + new Float:flDistanceClose = GetRandomFloat(0.0, flMaxRange * 0.33); + if (flDistanceClose < flMinRange) flDistanceClose = flMinRange; + + if (flChance >= 0.0 && flChance < flUpperBoundFar) iRange = 1; + else if (flChance >= flUpperBoundFar && flChance < flUpperBoundAverage) iRange = 2; + else if (flChance >= flUpperBoundAverage) iRange = 3; + + // Get a circle of positions around the player that we can appear in. + + // Create arrays first. + new Handle:hArrayFar = CreateArray(3); + new Handle:hArrayAverage = CreateArray(3); + new Handle:hArrayClose = CreateArray(3); + + // Set up our distances array. + decl Float:flDistances[3]; + flDistances[0] = flDistanceFar; + flDistances[1] = flDistanceAverage; + flDistances[2] = flDistanceClose; + + decl Float:hisEyePos[3], Float:hisEyeAng[3], Float:tempPos[3], Float:tempDir[3], Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; + GetClientEyePosition(iBestPlayer, hisEyePos); + GetClientEyeAngles(iBestPlayer, hisEyeAng); + + decl Handle:hTrace, index, Float:flHitNormal[3]; + decl Handle:hArray; + + decl Float:flTargetMins[3], Float:flTargetMaxs[3]; + if (!bProxy) + { + for (new i = 0; i < 3; i++) + { + flTargetMins[i] = g_flSlenderDetectMins[iBossIndex][i]; + flTargetMaxs[i] = g_flSlenderDetectMaxs[iBossIndex][i]; + } + } + else + { + GetEntPropVector(iProxyPlayer, Prop_Send, "m_vecMins", flTargetMins); + GetEntPropVector(iProxyPlayer, Prop_Send, "m_vecMaxs", flTargetMaxs); + } + + for (new i = 0; i < iRange; i++) + { + for (new Float:addAng = 0.0; addAng < 360.0; addAng += 7.5) + { + tempDir[0] = 0.0; + tempDir[1] = hisEyeAng[1] + addAng; + tempDir[2] = 0.0; + + GetAngleVectors(tempDir, tempDir, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(tempDir, tempDir); + ScaleVector(tempDir, flDistances[i]); + AddVectors(tempDir, hisEyePos, tempPos); + + // Drop to the ground if we're above ground using a TraceHull so IsSpaceOccupiedNPC can return true on something. + hTrace = TR_TraceRayFilterEx(tempPos, Float:{ 90.0, 0.0, 0.0 }, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitCharactersOrEntity, iBestPlayer); + TR_GetEndPosition(flBuffer, hTrace); + CloseHandle(hTrace); + + flBuffer2[0] = flTargetMins[0]; + flBuffer2[1] = flTargetMins[1]; + flBuffer2[2] = -flTargetMaxs[2]; + flBuffer3[0] = flTargetMaxs[0]; + flBuffer3[1] = flTargetMaxs[1]; + flBuffer3[2] = -flTargetMins[0]; + + if (GetVectorDistance(tempPos, flBuffer) >= 300.0) continue; + + // Drop dowwwwwn. + hTrace = TR_TraceHullFilterEx(tempPos, flBuffer, flBuffer2, flBuffer3, MASK_NPCSOLID, TraceRayDontHitCharactersOrEntity, iBestPlayer); + TR_GetEndPosition(tempPos, hTrace); + TR_GetPlaneNormal(hTrace, flHitNormal); + CloseHandle(hTrace); + + GetVectorAngles(flHitNormal, flHitNormal); + for (new i2 = 0; i2 < 3; i2++) flHitNormal[i2] = AngleNormalize(flHitNormal[i2]); + + tempPos[2] -= g_flSlenderDetectMaxs[iBossIndex][2]; + + if (TR_PointOutsideWorld(tempPos) + || (IsSpaceOccupiedNPC(tempPos, flTargetMins, flTargetMaxs, NPCGetEntIndex(iBossIndex))) + || (bProxy && IsSpaceOccupiedPlayer(tempPos, flTargetMins, flTargetMaxs, iProxyPlayer)) + || (flHitNormal[0] >= 0.0 && flHitNormal[0] < 45.0) + || (flHitNormal[0] < 0.0 && flHitNormal[0] > -45.0)) + { + continue; + } + + // Check if this position isn't too close to anyone else. + new bool:bTooClose = false; + + for (new i2 = 1; i2 <= MaxClients; i2++) + { + if (!IsClientInGame(i2) || !IsPlayerAlive(i2) || g_bPlayerEliminated[i2] || IsClientInGhostMode(i2)) continue; + GetClientAbsOrigin(i2, flBuffer); + if (GetVectorDistance(flBuffer, tempPos) < flMinRange) + { + bTooClose = true; + break; + } + } + + // Check if this position is too close to a boss. + if (!bTooClose) + { + decl iSlender; + for (new i2 = 0; i2 < MAX_BOSSES; i2++) + { + if (i2 == iBossIndex) continue; + if (NPCGetUniqueID(i2) == -1) continue; + + // If I'm a main boss, only check the distance between my copies and me. + if (g_iSlenderCopyMaster[iBossIndex] == -1) + { + if (g_iSlenderCopyMaster[i2] != iBossIndex) continue; + } + // If I'm a copy, just check with my other copy friends and my main boss. + else + { + new iMyMaster = g_iSlenderCopyMaster[iBossIndex]; + if (g_iSlenderCopyMaster[i2] != iMyMaster || i2 != iMyMaster) continue; + } + + iSlender = NPCGetEntIndex(i2); + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; + + SlenderGetAbsOrigin(i2, flBuffer); + if (GetVectorDistance(flBuffer, tempPos) < GetProfileFloat(sProfile, "teleport_dist_from_other_copies", 800.0)) + { + bTooClose = true; + break; + } + } + } + + if (bTooClose) continue; + + // Check from top to bottom of me. + + new bool:bCheckBlink = bool:GetProfileNum(sProfile, "teleport_use_blink"); + + // Check if my copy master or my fellow copies could see this position. + new bool:bDontAddPosition = false; + new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); + + decl Float:flCopyCheckPositions[6]; + for (new i2 = 0; i2 < 3; i2++) flCopyCheckPositions[i2] = tempPos[i2]; + for (new i2 = 3; i2 < 6; i2++) flCopyCheckPositions[i2] = tempPos[i2 - 3] + g_flSlenderEyePosOffset[iBossIndex][i2 - 3]; + + for (new i2 = 0; i2 < 2; i2++) + { + decl Float:flCopyCheckPos[3]; + for (new i3 = 0; i3 < 3; i3++) flCopyCheckPos[i3] = flCopyCheckPositions[i3 + (3 * i2)]; + + // Check the conditions first. + if (bVisiblePls) + { + if (!IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink) && + !IsPointVisibleToPlayer(iBestPlayer, flCopyCheckPos, _, bCheckBlink)) + { + bDontAddPosition = true; + break; + } + } + else if (bBeCreepy) + { + if (!IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink) && + IsPointVisibleToAPlayer(flCopyCheckPos, false, bCheckBlink) && + IsPointVisibleToPlayer(iBestPlayer, flCopyCheckPos, false, bCheckBlink)) + { + // Do nothing. + } + else + { + continue; + } + } + else + { + if (IsPointVisibleToAPlayer(flCopyCheckPos, _, bCheckBlink)) + { + bDontAddPosition = true; + break; + } + } + + for (new i3 = 0; i3 < MAX_BOSSES; i3++) + { + if (i3 == iBossIndex) continue; + if (NPCGetUniqueID(i3) == -1) continue; + + new iBoss = NPCGetEntIndex(i3); + if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; + + if (i3 == iCopyMaster || + (iCopyMaster != -1 && NPCGetFromUniqueID(g_iSlenderCopyMaster[i3]) == iCopyMaster)) + { + } + else continue; + + decl Float:flCopyPos[3]; + SlenderGetEyePosition(i3, flCopyPos); + hTrace = TR_TraceRayFilterEx(flCopyPos, + flCopyCheckPos, + CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, + RayType_EndPoint, + TraceRayBossVisibility, + iBoss); + + bDontAddPosition = !TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (!bDontAddPosition) + { + decl Float:flCopyMins[3], Float:flCopyMaxs[3]; + GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flCopyPos); + GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flCopyMins); + GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flCopyMaxs); + + for (new i4 = 0; i4 < 3; i4++) flCopyPos[i4] += ((flCopyMins[i4] + flCopyMaxs[i4]) / 2.0); + + hTrace = TR_TraceRayFilterEx(flCopyPos, + flCopyCheckPos, + CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, + RayType_EndPoint, + TraceRayBossVisibility, + iBoss); + + bDontAddPosition = !TR_DidHit(hTrace); + CloseHandle(hTrace); + } + + if (bDontAddPosition) break; + } + + if (bDontAddPosition) break; + } + + if (bDontAddPosition) continue; + + // Insert the vector into our array. Choose which one, first. + // We're just using hArray as a variable to store the correct array, not the array itself. All arrays will be closed at the end. + if (i == 0) hArray = hArrayFar; + else if (i == 1) hArray = hArrayAverage; + else if (i == 2) hArray = hArrayClose; + + index = PushArrayCell(hArray, tempPos[0]); + SetArrayCell(hArray, index, tempPos[1], 1); + SetArrayCell(hArray, index, tempPos[2], 2); + } + } + + new size; + if ((size = GetArraySize(hArrayClose)) > 0) + { + index = GetRandomInt(0, size - 1); + buffer[0] = Float:GetArrayCell(hArrayClose, index); + buffer[1] = Float:GetArrayCell(hArrayClose, index, 1); + buffer[2] = Float:GetArrayCell(hArrayClose, index, 2); + } + else if ((size = GetArraySize(hArrayAverage)) > 0) + { + index = GetRandomInt(0, size - 1); + buffer[0] = Float:GetArrayCell(hArrayAverage, index); + buffer[1] = Float:GetArrayCell(hArrayAverage, index, 1); + buffer[2] = Float:GetArrayCell(hArrayAverage, index, 2); + } + else if ((size = GetArraySize(hArrayFar)) > 0) + { + index = GetRandomInt(0, size - 1); + buffer[0] = Float:GetArrayCell(hArrayFar, index); + buffer[1] = Float:GetArrayCell(hArrayFar, index, 1); + buffer[2] = Float:GetArrayCell(hArrayFar, index, 2); + } + else + { + CloseHandle(hArrayClose); + CloseHandle(hArrayAverage); + CloseHandle(hArrayFar); + +#if defined DEBUG + if (GetConVarBool(g_cvDebugBosses)) PrintToChatAll("SlenderCalculateNewPlace(%d) failed: no locations available", iBossIndex); +#endif + + return false; + } + + CloseHandle(hArrayClose); + CloseHandle(hArrayAverage); + CloseHandle(hArrayFar); + return true; +} + +bool:SlenderMarkAsFake(iBossIndex) +{ + new iBossFlags = NPCGetFlags(iBossIndex); + if (iBossFlags & SFF_MARKEDASFAKE) return false; + + new slender = NPCGetEntIndex(iBossIndex); + new iSlenderModel = EntRefToEntIndex(g_iSlenderModel[iBossIndex]); + g_iSlender[iBossIndex] = INVALID_ENT_REFERENCE; + g_iSlenderModel[iBossIndex] = INVALID_ENT_REFERENCE; + + NPCSetFlags(iBossIndex, iBossFlags | SFF_MARKEDASFAKE); + + g_hSlenderFakeTimer[iBossIndex] = CreateTimer(3.0, Timer_SlenderMarkedAsFake, iBossIndex, TIMER_FLAG_NO_MAPCHANGE); + + if (slender && slender != INVALID_ENT_REFERENCE) + { + CreateTimer(2.0, Timer_KillEntity, EntIndexToEntRef(slender), TIMER_FLAG_NO_MAPCHANGE); + + new iFlags = GetEntProp(slender, Prop_Send, "m_usSolidFlags"); + if (!(iFlags & 0x0004)) iFlags |= 0x0004; // FSOLID_NOT_SOLID + if (!(iFlags & 0x0008)) iFlags |= 0x0008; // FSOLID_TRIGGER + SetEntProp(slender, Prop_Send, "m_usSolidFlags", iFlags); + } + + if (iSlenderModel && iSlenderModel != INVALID_ENT_REFERENCE) + { + SetVariantFloat(0.0); + AcceptEntityInput(iSlenderModel, "SetPlaybackRate"); + SetEntityRenderFx(iSlenderModel, RENDERFX_FADE_FAST); + } + + return true; +} + +public Action:Timer_SlenderMarkedAsFake(Handle:timer, any:data) +{ + if (timer != g_hSlenderFakeTimer[data]) return; + + NPCRemove(data); +} + +stock SpawnSlenderModel(iBossIndex, const Float:pos[3]) +{ + if (NPCGetUniqueID(iBossIndex) == -1) + { + LogError("Could not spawn boss model: boss does not exist!"); + return -1; + } + + new iProfileIndex = NPCGetProfileIndex(iBossIndex); + + decl String:buffer[PLATFORM_MAX_PATH], String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + GetProfileString(sProfile, "model", buffer, sizeof(buffer)); + if (!buffer[0]) + { + LogError("Could not spawn boss model: model is invalid!"); + return -1; + } + + new Float:flModelScale = NPCGetModelScale(iBossIndex); + if (flModelScale <= 0.0) + { + LogError("Could not spawn boss model: model scale is less than or equal to 0.0!"); + return -1; + } + + new iSlenderModel = CreateEntityByName("prop_dynamic_override"); + if (iSlenderModel != -1) + { + SetEntityModel(iSlenderModel, buffer); + + TeleportEntity(iSlenderModel, pos, NULL_VECTOR, NULL_VECTOR); + DispatchSpawn(iSlenderModel); + ActivateEntity(iSlenderModel); + + SetEntProp(iSlenderModel, Prop_Send, "m_nSkin", GetBossProfileSkin(iProfileIndex)); + SetEntProp(iSlenderModel, Prop_Send, "m_nBody", GetBossProfileBodyGroups(iProfileIndex)); + + GetProfileString(sProfile, "animation_idle", buffer, sizeof(buffer)); + if (buffer[0]) + { + SetVariantString(buffer); + AcceptEntityInput(iSlenderModel, "SetDefaultAnimation"); + SetVariantString(buffer); + AcceptEntityInput(iSlenderModel, "SetAnimation"); + AcceptEntityInput(iSlenderModel, "DisableCollision"); + } + + SetVariantFloat(GetProfileFloat(sProfile, "animation_idle_playbackrate", 1.0)); + AcceptEntityInput(iSlenderModel, "SetPlaybackRate"); + + SetEntPropFloat(iSlenderModel, Prop_Send, "m_flModelScale", flModelScale); + + // Create special effects. + SetEntityRenderMode(iSlenderModel, RenderMode:GetProfileNum(sProfile, "effect_rendermode", _:RENDER_NORMAL)); + SetEntityRenderFx(iSlenderModel, RenderFx:GetProfileNum(sProfile, "effect_renderfx", _:RENDERFX_NONE)); + + decl iColor[4]; + GetProfileColor(sProfile, "effect_rendercolor", iColor[0], iColor[1], iColor[2], iColor[3]); + SetEntityRenderColor(iSlenderModel, iColor[0], iColor[1], iColor[2], iColor[3]); + + KvRewind(g_hConfig); + if (KvJumpToKey(g_hConfig, sProfile) && + KvJumpToKey(g_hConfig, "effects") && + KvGotoFirstSubKey(g_hConfig)) + { + do + { + + } + while KvGotoNextKey(g_hConfig); + } + } + + return iSlenderModel; +} + +stock bool:PlayerCanSeeSlender(client, iBossIndex, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) +{ + return IsNPCVisibleToPlayer(iBossIndex, client, bCheckFOV, bCheckBlink, bCheckEliminated); +} + +stock bool:PeopleCanSeeSlender(iBossIndex, bool:bCheckFOV=true, bool:bCheckBlink=false) +{ + return IsNPCVisibleToAPlayer(iBossIndex, bCheckFOV, bCheckBlink); +} + +// TODO: bCheckBlink and bCheckEliminated should NOT be function arguments! +bool:IsNPCVisibleToPlayer(iNPCIndex, client, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) +{ + if (!NPCIsValid(iNPCIndex)) return false; + + new iNPC = NPCGetEntIndex(iNPCIndex); + if (iNPC && iNPC != INVALID_ENT_REFERENCE) + { + decl Float:flEyePos[3]; + NPCGetEyePosition(iNPCIndex, flEyePos); + return IsPointVisibleToPlayer(client, flEyePos, bCheckFOV, bCheckBlink, bCheckEliminated); + } + + return false; +} + +// TODO: bCheckBlink and bCheckEliminated should NOT be function arguments! +bool:IsNPCVisibleToAPlayer(iNPCIndex, bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) +{ + for (new client = 1; client <= MaxClients; client++) + { + if (IsNPCVisibleToPlayer(iNPCIndex, client, bCheckFOV, bCheckBlink, bCheckEliminated)) + { + return true; + } + } + + return false; +} + +Float:NPCGetDistanceFromPoint(iNPCIndex, const Float:flPoint[3], bool:bSquared=false) +{ + new iNPC = NPCGetEntIndex(iNPCIndex); + if (iNPC && iNPC != INVALID_ENT_REFERENCE) + { + decl Float:flPos[3]; + SlenderGetAbsOrigin(iNPCIndex, flPos); + + return GetVectorDistance(flPos, flPoint, bSquared); + } + + return -1.0; +} + +Float:NPCGetDistanceFromEntity(iNPCIndex, ent, bool:bSquared=false) +{ + if (!IsValidEntity(ent)) return -1.0; + + decl Float:flPos[3]; + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); + + return NPCGetDistanceFromPoint(iNPCIndex, flPos, bSquared); +} + +public bool:TraceRayBossVisibility(entity, mask, any:data) +{ + if (entity == data || IsValidClient(entity)) return false; + + new iBossIndex = NPCGetFromEntIndex(entity); + if (iBossIndex != -1) return false; + + if (IsValidEdict(entity)) + { + decl String:sClass[64]; + GetEntityNetClass(entity, sClass, sizeof(sClass)); + + if (StrEqual(sClass, "CTFAmmoPack")) return false; + } + + return true; +} + +public bool:TraceRayDontHitCharacters(entity, mask, any:data) +{ + if (entity > 0 && entity <= MaxClients) return false; + + new iBossIndex = NPCGetFromEntIndex(entity); + if (iBossIndex != -1) return false; + + return true; +} + +public bool:TraceRayDontHitCharactersOrEntity(entity, mask, any:data) +{ + if (entity == data) return false; + + if (entity > 0 && entity <= MaxClients) return false; + + new iBossIndex = NPCGetFromEntIndex(entity); + if (iBossIndex != -1) return false; + + return true; +} + +#include "rytp_horror/npc/npc_chaser.sp" diff --git a/addons/sourcemod/scripting/rytp_horror/npc/npc_chaser.sp b/addons/sourcemod/scripting/rytp_horror/npc/npc_chaser.sp index e4b9b49..b1f5c67 100644 --- a/addons/sourcemod/scripting/rytp_horror/npc/npc_chaser.sp +++ b/addons/sourcemod/scripting/rytp_horror/npc/npc_chaser.sp @@ -1,2515 +1,2515 @@ -#if defined _sf2_npc_chaser_included - #endinput -#endif -#define _sf2_npc_chaser_included - -static Float:g_flNPCStepSize[MAX_BOSSES]; - -static Float:g_flNPCWalkSpeed[MAX_BOSSES][Difficulty_Max]; -static Float:g_flNPCAirSpeed[MAX_BOSSES][Difficulty_Max]; - -static Float:g_flNPCMaxWalkSpeed[MAX_BOSSES][Difficulty_Max]; -static Float:g_flNPCMaxAirSpeed[MAX_BOSSES][Difficulty_Max]; - -static Float:g_flNPCWakeRadius[MAX_BOSSES]; - -static bool:g_bNPCStunEnabled[MAX_BOSSES]; -static Float:g_flNPCStunDuration[MAX_BOSSES]; -static bool:g_bNPCStunFlashlightEnabled[MAX_BOSSES]; -static Float:g_flNPCStunFlashlightDamage[MAX_BOSSES]; -static Float:g_flNPCStunInitialHealth[MAX_BOSSES]; -static Float:g_flNPCStunHealth[MAX_BOSSES]; - -static g_iNPCState[MAX_BOSSES] = { -1, ... }; -static g_iNPCMovementActivity[MAX_BOSSES] = { -1, ... }; - -enum SF2NPCChaser_BaseAttackStructure -{ - SF2NPCChaser_BaseAttackType, - Float:SF2NPCChaser_BaseAttackDamage, - Float:SF2NPCChaser_BaseAttackDamageVsProps, - Float:SF2NPCChaser_BaseAttackDamageForce, - SF2NPCChaser_BaseAttackDamageType, - Float:SF2NPCChaser_BaseAttackDamageDelay, - Float:SF2NPCChaser_BaseAttackRange, - Float:SF2NPCChaser_BaseAttackDuration, - Float:SF2NPCChaser_BaseAttackSpread, - Float:SF2NPCChaser_BaseAttackBeginRange, - Float:SF2NPCChaser_BaseAttackBeginFOV, - Float:SF2NPCChaser_BaseAttackCooldown, - Float:SF2NPCChaser_BaseAttackNextAttackTime -}; - -static g_NPCBaseAttacks[MAX_BOSSES][SF2_CHASER_BOSS_MAX_ATTACKS][SF2NPCChaser_BaseAttackStructure]; - -#if defined METHODMAPS - -const SF2NPC_Chaser SF2_INVALID_NPC_CHASER = SF2NPC_Chaser:-1; - - -methodmap SF2NPC_Chaser < SF2NPC_BaseNPC -{ - property float WakeRadius - { - public get() { return NPCChaserGetWakeRadius(this.Index); } - } - - property float StepSize - { - public get() { return NPCChaserGetStepSize(this.Index); } - } - - property bool StunEnabled - { - public get() { return NPCChaserIsStunEnabled(this.Index); } - } - - property bool StunByFlashlightEnabled - { - public get() { return NPCChaserIsStunByFlashlightEnabled(this.Index); } - } - - property float StunFlashlightDamage - { - public get() { return NPCChaserGetStunFlashlightDamage(this.Index); } - } - - property float StunDuration - { - public get() { return NPCChaserGetStunDuration(this.Index); } - } - - property float StunHealth - { - public get() { return NPCChaserGetStunHealth(this.Index); } - public set(float amount) { NPCChaserSetStunHealth(this.Index, amount); } - } - - property float StunInitialHealth - { - public get() { return NPCChaserGetStunInitialHealth(this.Index); } - } - - property int State - { - public get() { return NPCChaserGetState(this.Index); } - public set(int state) { NPCChaserSetState(this.Index, state); } - } - - property int MovementActivity - { - public get() { return NPCChaserGetMovementActivity(this.Index); } - public set(int movementActivity) { NPCChaserSetMovementActivity(this.Index, movementActivity); } - } - - public SF2NPC_Chaser(int index) - { - return SF2NPC_Chaser:SF2NPC_BaseNPC(index); - } - - public float GetWalkSpeed(int difficulty) - { - return NPCChaserGetWalkSpeed(this.Index, difficulty); - } - - public void SetWalkSpeed(int difficulty, float amount) - { - NPCChaserSetWalkSpeed(this.Index, difficulty, amount); - } - - public float GetAirSpeed(int difficulty) - { - return NPCChaserGetAirSpeed(this.Index, difficulty); - } - - public void SetAirSpeed(int difficulty, float amount) - { - NPCChaserSetAirSpeed(this.Index, difficulty, amount); - } - - public float GetMaxWalkSpeed(int difficulty) - { - return NPCChaserGetMaxWalkSpeed(this.Index, difficulty); - } - - public void SetMaxWalkSpeed(int difficulty, float amount) - { - NPCChaserSetMaxWalkSpeed(this.Index, difficulty, amount); - } - - public float GetMaxAirSpeed(int difficulty) - { - return NPCChaserGetMaxAirSpeed(this.Index, difficulty); - } - - public void SetMaxAirSpeed(int difficulty, float amount) - { - NPCChaserSetMaxAirSpeed(this.Index, difficulty, amount); - } - - public void AddStunHealth(float amount) - { - NPCChaserAddStunHealth(this.Index, amount); - } -} - -#endif - -public NPCChaserInitialize() -{ - for (new iNPCIndex = 0; iNPCIndex < MAX_BOSSES; iNPCIndex++) - { - NPCChaserResetValues(iNPCIndex); - } -} - -stock Float:NPCChaserGetWalkSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCWalkSpeed[iNPCIndex][iDifficulty]; -} - -stock NPCChaserSetWalkSpeed(iNPCIndex, iDifficulty, Float:flAmount) -{ - g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = flAmount; -} - -stock Float:NPCChaserGetAirSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCAirSpeed[iNPCIndex][iDifficulty]; -} - -stock NPCChaserSetAirSpeed(iNPCIndex, iDifficulty, Float:flAmount) -{ - g_flNPCAirSpeed[iNPCIndex][iDifficulty] = flAmount; -} - -stock Float:NPCChaserGetMaxWalkSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty]; -} - -stock NPCChaserSetMaxWalkSpeed(iNPCIndex, iDifficulty, Float:flAmount) -{ - g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = flAmount; -} - -stock Float:NPCChaserGetMaxAirSpeed(iNPCIndex, iDifficulty) -{ - return g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty]; -} - -stock NPCChaserSetMaxAirSpeed(iNPCIndex, iDifficulty, Float:flAmount) -{ - g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = flAmount; -} - -stock Float:NPCChaserGetWakeRadius(iNPCIndex) -{ - return g_flNPCWakeRadius[iNPCIndex]; -} - -stock Float:NPCChaserGetStepSize(iNPCIndex) -{ - return g_flNPCStepSize[iNPCIndex]; -} - -stock NPCChaserGetAttackType(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackType]; -} - -stock Float:NPCChaserGetAttackDamage(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamage]; -} - -stock Float:NPCChaserGetAttackDamageVsProps(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageVsProps]; -} - -stock Float:NPCChaserGetAttackDamageForce(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageForce]; -} - -stock NPCChaserGetAttackDamageType(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageType]; -} - -stock Float:NPCChaserGetAttackDamageDelay(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageDelay]; -} - -stock Float:NPCChaserGetAttackRange(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackRange]; -} - -stock Float:NPCChaserGetAttackDuration(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDuration]; -} - -stock Float:NPCChaserGetAttackSpread(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackSpread]; -} - -stock Float:NPCChaserGetAttackBeginRange(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackBeginRange]; -} - -stock Float:NPCChaserGetAttackBeginFOV(iNPCIndex, iAttackIndex) -{ - return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackBeginFOV]; -} - -stock bool:NPCChaserIsStunEnabled(iNPCIndex) -{ - return g_bNPCStunEnabled[iNPCIndex]; -} - -stock bool:NPCChaserIsStunByFlashlightEnabled(iNPCIndex) -{ - return g_bNPCStunFlashlightEnabled[iNPCIndex]; -} - -stock Float:NPCChaserGetStunFlashlightDamage(iNPCIndex) -{ - return g_flNPCStunFlashlightDamage[iNPCIndex]; -} - -stock Float:NPCChaserGetStunDuration(iNPCIndex) -{ - return g_flNPCStunDuration[iNPCIndex]; -} - -stock Float:NPCChaserGetStunHealth(iNPCIndex) -{ - return g_flNPCStunHealth[iNPCIndex]; -} - -stock NPCChaserSetStunHealth(iNPCIndex, Float:flAmount) -{ - g_flNPCStunHealth[iNPCIndex] = flAmount; -} - -stock NPCChaserAddStunHealth(iNPCIndex, Float:flAmount) -{ - NPCChaserSetStunHealth(iNPCIndex, NPCChaserGetStunHealth(iNPCIndex) + flAmount); -} - -stock Float:NPCChaserGetStunInitialHealth(iNPCIndex) -{ - return g_flNPCStunInitialHealth[iNPCIndex]; -} - -stock NPCChaserGetState(iNPCIndex) -{ - return g_iNPCState[iNPCIndex]; -} - -stock NPCChaserSetState(iNPCIndex, iState) -{ - g_iNPCState[iNPCIndex] = iState; -} - -stock NPCChaserGetMovementActivity(iNPCIndex) -{ - return g_iNPCMovementActivity[iNPCIndex]; -} - -stock NPCChaserSetMovementActivity(iNPCIndex, iMovementActivity) -{ - g_iNPCMovementActivity[iNPCIndex] = iMovementActivity; -} - -stock NPCChaserOnSelectProfile(iNPCIndex) -{ - new iUniqueProfileIndex = NPCGetUniqueProfileIndex(iNPCIndex); - - g_flNPCWakeRadius[iNPCIndex] = GetChaserProfileWakeRadius(iUniqueProfileIndex); - g_flNPCStepSize[iNPCIndex] = GetChaserProfileStepSize(iUniqueProfileIndex); - - for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) - { - g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = GetChaserProfileWalkSpeed(iUniqueProfileIndex, iDifficulty); - g_flNPCAirSpeed[iNPCIndex][iDifficulty] = GetChaserProfileAirSpeed(iUniqueProfileIndex, iDifficulty); - - g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = GetChaserProfileMaxWalkSpeed(iUniqueProfileIndex, iDifficulty); - g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = GetChaserProfileMaxAirSpeed(iUniqueProfileIndex, iDifficulty); - } - - // Get attack data. - for (new i = 0; i < GetChaserProfileAttackCount(iUniqueProfileIndex); i++) - { - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackType] = GetChaserProfileAttackType(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamage] = GetChaserProfileAttackDamage(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageVsProps] = GetChaserProfileAttackDamageVsProps(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageForce] = GetChaserProfileAttackDamageForce(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageType] = GetChaserProfileAttackDamageType(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageDelay] = GetChaserProfileAttackDamageDelay(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackRange] = GetChaserProfileAttackRange(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDuration] = GetChaserProfileAttackDuration(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackSpread] = GetChaserProfileAttackSpread(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginRange] = GetChaserProfileAttackBeginRange(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginFOV] = GetChaserProfileAttackBeginFOV(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackCooldown] = GetChaserProfileAttackCooldown(iUniqueProfileIndex, i); - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackNextAttackTime] = -1.0; - } - - // Get stun data. - g_bNPCStunEnabled[iNPCIndex] = GetChaserProfileStunState(iUniqueProfileIndex); - g_flNPCStunDuration[iNPCIndex] = GetChaserProfileStunDuration(iUniqueProfileIndex); - g_bNPCStunFlashlightEnabled[iNPCIndex] = GetChaserProfileStunFlashlightState(iUniqueProfileIndex); - g_flNPCStunFlashlightDamage[iNPCIndex] = GetChaserProfileStunFlashlightDamage(iUniqueProfileIndex); - g_flNPCStunInitialHealth[iNPCIndex] = GetChaserProfileStunHealth(iUniqueProfileIndex); - - NPCChaserSetStunHealth(iNPCIndex, NPCChaserGetStunInitialHealth(iNPCIndex)); -} - -NPCChaserOnRemoveProfile(iNPCIndex) -{ - NPCChaserResetValues(iNPCIndex); -} - -/** - * Resets all global variables on a specified NPC. Usually this should be done last upon removing a boss from the game. - */ -static NPCChaserResetValues(iNPCIndex) -{ - g_flNPCWakeRadius[iNPCIndex] = 0.0; - g_flNPCStepSize[iNPCIndex] = 0.0; - - for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) - { - g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = 0.0; - g_flNPCAirSpeed[iNPCIndex][iDifficulty] = 0.0; - - g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = 0.0; - g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = 0.0; - } - - // Clear attack data. - for (new i = 0; i < SF2_CHASER_BOSS_MAX_ATTACKS; i++) - { - // Base attack data. - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackType] = SF2BossAttackType_Invalid; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamage] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageVsProps] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageForce] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageType] = 0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageDelay] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackRange] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDuration] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackSpread] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginRange] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginFOV] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackCooldown] = 0.0; - g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackNextAttackTime] = -1.0; - } - - g_bNPCStunEnabled[iNPCIndex] = false; - g_flNPCStunDuration[iNPCIndex] = 0.0; - g_bNPCStunFlashlightEnabled[iNPCIndex] = false; - g_flNPCStunInitialHealth[iNPCIndex] = 0.0; - - NPCChaserSetStunHealth(iNPCIndex, 0.0); - - g_iNPCState[iNPCIndex] = -1; - g_iNPCMovementActivity[iNPCIndex] = -1; -} - -// So this is how the thought process of the bosses should go. -// 1. Search for enemy; either by sight or by sound. -// - Any noticeable sounds should be investigated. -// - Too many sounds will put me in alert mode. -// 2. Alert of an enemy; I saw something or I heard something unusual -// - Go to the position where I last heard the sound. -// - Keep on searching until I give up. Then drop back to idle mode. -// 3. Found an enemy! Give chase! -// - Keep on chasing until enemy is killed or I give up. -// - Keep a path in memory as long as I still have him in my sights. -// - If I lose sight or I'm unable to traverse safely, find paths around obstacles and follow memorized path. -// - If I reach the end of my path and I still don't see him and I still want to pursue him, keep on going in the direction I'm going. - -stock bool:IsTargetValidForSlender(iTarget, bool:bIncludeEliminated=false) -{ - if (!iTarget || !IsValidEntity(iTarget)) return false; - - if (IsValidClient(iTarget)) - { - if (!IsClientInGame(iTarget) || - !IsPlayerAlive(iTarget) || - IsClientInDeathCam(iTarget) || - (!bIncludeEliminated && g_bPlayerEliminated[iTarget]) || - IsClientInGhostMode(iTarget) || - DidClientEscape(iTarget)) return false; - } - - return true; -} - -public Action:Timer_SlenderChaseBossThink(Handle:timer, any:entref) -{ - if (!g_bEnabled) return Plugin_Stop; - - new slender = EntRefToEntIndex(entref); - if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return Plugin_Stop; - - if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; - - if (NPCGetFlags(iBossIndex) & SFF_MARKEDASFAKE) return Plugin_Stop; - - decl Float:flSlenderVelocity[3], Float:flMyPos[3], Float:flMyEyeAng[3]; - new Float:flBuffer[3]; - - decl String:sSlenderProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sSlenderProfile, sizeof(sSlenderProfile)); - - GetEntPropVector(slender, Prop_Data, "m_vecAbsVelocity", flSlenderVelocity); - GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flMyPos); - GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", flMyEyeAng); - AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[iBossIndex], flMyEyeAng); - for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); - - new iDifficulty = GetConVarInt(g_cvDifficulty); - - new Float:flVelocityRatio; - new Float:flVelocityRatioWalk; - - new Float:flOriginalSpeed = NPCGetSpeed(iBossIndex, iDifficulty); - new Float:flOriginalWalkSpeed = NPCChaserGetWalkSpeed(iBossIndex, iDifficulty); - new Float:flMaxSpeed = NPCGetMaxSpeed(iBossIndex, iDifficulty); - new Float:flMaxWalkSpeed = NPCChaserGetMaxWalkSpeed(iBossIndex, iDifficulty); - - new Float:flSpeed = flOriginalSpeed * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier; - if (flSpeed < flOriginalSpeed) flSpeed = flOriginalSpeed; - if (flSpeed > flMaxSpeed) flSpeed = flMaxSpeed; - - new Float:flWalkSpeed = flOriginalWalkSpeed * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier; - if (flWalkSpeed < flOriginalWalkSpeed) flWalkSpeed = flOriginalWalkSpeed; - if (flWalkSpeed > flMaxWalkSpeed) flWalkSpeed = flMaxWalkSpeed; - - if (PeopleCanSeeSlender(iBossIndex, _, false)) - { - if (NPCHasAttribute(iBossIndex, "reduced speed on look")) - { - flSpeed *= NPCGetAttributeValue(iBossIndex, "reduced speed on look"); - } - - if (NPCHasAttribute(iBossIndex, "reduced walk speed on look")) - { - flWalkSpeed *= NPCGetAttributeValue(iBossIndex, "reduced walk speed on look"); - } - } - - g_flSlenderCalculatedWalkSpeed[iBossIndex] = flWalkSpeed; - g_flSlenderCalculatedSpeed[iBossIndex] = flSpeed; - - if (flOriginalSpeed <= 0.0) flVelocityRatio = 0.0; - else flVelocityRatio = GetVectorLength(flSlenderVelocity) / flOriginalSpeed; - - if (flOriginalWalkSpeed <= 0.0) flVelocityRatioWalk = 0.0; - else flVelocityRatioWalk = GetVectorLength(flSlenderVelocity) / flOriginalWalkSpeed; - - new Float:flAttackRange = NPCChaserGetAttackRange(iBossIndex, 0); - new Float:flAttackFOV = NPCChaserGetAttackSpread(iBossIndex, 0); - new Float:flAttackBeginRange = NPCChaserGetAttackBeginRange(iBossIndex, 0); - new Float:flAttackBeginFOV = NPCChaserGetAttackBeginFOV(iBossIndex, 0); - - - new iOldState = g_iSlenderState[iBossIndex]; - new iOldTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); - - new iBestNewTarget = INVALID_ENT_REFERENCE; - new Float:flSearchRange = NPCGetSearchRadius(iBossIndex); - new Float:flBestNewTargetDist = flSearchRange; - new iState = iOldState; - - new bool:bPlayerInFOV[MAXPLAYERS + 1]; - new bool:bPlayerNear[MAXPLAYERS + 1]; - new Float:flPlayerDists[MAXPLAYERS + 1]; - new bool:bPlayerVisible[MAXPLAYERS + 1]; - - new bool:bAttackEliminated = bool:(NPCGetFlags(iBossIndex) & SFF_ATTACKWAITERS); - new bool:bStunEnabled = NPCChaserIsStunEnabled(iBossIndex); - - decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; - GetEntPropVector(slender, Prop_Send, "m_vecMins", flSlenderMins); - GetEntPropVector(slender, Prop_Send, "m_vecMaxs", flSlenderMaxs); - - decl Float:flTraceMins[3], Float:flTraceMaxs[3]; - flTraceMins[0] = flSlenderMins[0]; - flTraceMins[1] = flSlenderMins[1]; - flTraceMins[2] = 0.0; - flTraceMaxs[0] = flSlenderMaxs[0]; - flTraceMaxs[1] = flSlenderMaxs[1]; - flTraceMaxs[2] = 0.0; - - // Gather data about the players around me and get the best new target, in case my old target is invalidated. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsTargetValidForSlender(i, bAttackEliminated)) continue; - - decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; - NPCGetEyePosition(iBossIndex, flTraceStartPos); - GetClientEyePosition(i, flTraceEndPos); - - new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flTraceEndPos, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayBossVisibility, - slender); - - new bool:bIsVisible = !TR_DidHit(hTrace); - new iTraceHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (!bIsVisible && iTraceHitEntity == i) bIsVisible = true; - - bPlayerVisible[i] = bIsVisible; - - // Near radius check. - if (bIsVisible && - GetVectorDistance(flTraceStartPos, flTraceEndPos) <= NPCChaserGetWakeRadius(iBossIndex)) - { - bPlayerNear[i] = true; - } - - // FOV check. - SubtractVectors(flTraceEndPos, flTraceStartPos, flBuffer); - GetVectorAngles(flBuffer, flBuffer); - - if (FloatAbs(AngleDiff(flMyEyeAng[1], flBuffer[1])) <= (NPCGetFOV(iBossIndex) * 0.5)) - { - bPlayerInFOV[i] = true; - } - - new Float:flDist; - new Float:flPriorityValue = g_iPageMax > 0 ? (float(g_iPlayerPageCount[i]) / float(g_iPageMax)) : 0.0; - - if (TF2_GetPlayerClass(i) == TFClass_Medic) flPriorityValue += 0.72; - - flDist = GetVectorDistance(flTraceStartPos, flTraceEndPos); - flPlayerDists[i] = flDist; - - if ((bPlayerNear[i] && iState != STATE_CHASE && iState != STATE_ALERT) || (bIsVisible && bPlayerInFOV[i])) - { - decl Float:flTargetPos[3]; - GetClientAbsOrigin(i, flTargetPos); - - if (flDist <= flSearchRange) - { - // Subtract distance to increase priority. - flDist -= (flDist * flPriorityValue); - - if (flDist < flBestNewTargetDist) - { - iBestNewTarget = i; - flBestNewTargetDist = flDist; - g_iSlenderInterruptConditions[iBossIndex] |= COND_SAWENEMY; - } - - g_flSlenderLastFoundPlayer[iBossIndex][i] = GetGameTime(); - g_flSlenderLastFoundPlayerPos[iBossIndex][i][0] = flTargetPos[0]; - g_flSlenderLastFoundPlayerPos[iBossIndex][i][1] = flTargetPos[1]; - g_flSlenderLastFoundPlayerPos[iBossIndex][i][2] = flTargetPos[2]; - } - } - } - - new bool:bInFlashlight = false; - - // Check to see if someone is facing at us with flashlight on. Only if I'm facing them too. BLINDNESS! - for (new i = 1; i <= MaxClients; i++) - { - if (!IsTargetValidForSlender(i, bAttackEliminated)) continue; - - if (!IsClientUsingFlashlight(i) || !bPlayerInFOV[i]) continue; - - decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; - GetClientEyePosition(i, flTraceStartPos); - NPCGetEyePosition(iBossIndex, flTraceEndPos); - - if (GetVectorDistance(flTraceStartPos, flTraceEndPos) <= SF2_FLASHLIGHT_LENGTH) - { - decl Float:flEyeAng[3], Float:flRequiredAng[3]; - GetClientEyeAngles(i, flEyeAng); - SubtractVectors(flTraceEndPos, flTraceStartPos, flRequiredAng); - GetVectorAngles(flRequiredAng, flRequiredAng); - - if ((FloatAbs(AngleDiff(flEyeAng[0], flRequiredAng[0])) + FloatAbs(AngleDiff(flEyeAng[1], flRequiredAng[1]))) <= 45.0) - { - new Handle:hTrace = TR_TraceRayFilterEx(flTraceStartPos, - flTraceEndPos, - MASK_PLAYERSOLID, - RayType_EndPoint, - TraceRayBossVisibility, - slender); - - new bool:bDidHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (!bDidHit) - { - bInFlashlight = true; - break; - } - } - } - } - - // Damage us if we're in a flashlight. - if (bInFlashlight) - { - if (bStunEnabled) - { - if (NPCChaserIsStunByFlashlightEnabled(iBossIndex)) - { - if (NPCChaserGetStunHealth(iBossIndex) > 0) - { - NPCChaserAddStunHealth(iBossIndex, -NPCChaserGetStunFlashlightDamage(iBossIndex)); - } - } - } - } - - // Process the target that we should have. - new iTarget = iOldTarget; - - /* - if (IsValidEdict(iBestNewTarget)) - { - iTarget = iBestNewTarget; - g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); - } - */ - - if (iTarget && iTarget != INVALID_ENT_REFERENCE) - { - if (!IsTargetValidForSlender(iTarget, bAttackEliminated)) - { - // Clear our target; he's not valid anymore. - iOldTarget = iTarget; - iTarget = INVALID_ENT_REFERENCE; - g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; - } - } - else - { - // Clear our target; he's not valid anymore. - iOldTarget = iTarget; - iTarget = INVALID_ENT_REFERENCE; - g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; - } - - new iInterruptConditions = g_iSlenderInterruptConditions[iBossIndex]; - new bool:bQueueForNewPath = false; - - // Process which state we should be in. - switch (iState) - { - case STATE_IDLE, STATE_WANDER: - { - if (iState == STATE_WANDER) - { - if (GetArraySize(g_hSlenderPath[iBossIndex]) <= 0) - { - iState = STATE_IDLE; - } - } - else - { - if (GetGameTime() >= g_flSlenderNextWanderPos[iBossIndex] && GetRandomFloat(0.0, 1.0) <= 0.25) - { - iState = STATE_WANDER; - } - } - - if (iInterruptConditions & COND_SAWENEMY) - { - // I saw someone over here. Automatically put me into alert mode. - iState = STATE_ALERT; - } - else if (iInterruptConditions & COND_HEARDSUSPICIOUSSOUND) - { - // Sound counts: - // +1 will be added if it hears a footstep. - // +2 will be added if the footstep is someone sprinting. - // +5 will be added if the sound is from a player's weapon hitting an object. - // +10 will be added if a voice command is heard. - // - // Sound counts will be reset after the boss hears a sound after a certain amount of time. - // The purpose of sound counts is to induce boss focusing on sounds suspicious entities are making. - - new iCount = 0; - if (iInterruptConditions & COND_HEARDFOOTSTEP) iCount += 1; - if (iInterruptConditions & COND_HEARDFOOTSTEPLOUD) iCount += 2; - if (iInterruptConditions & COND_HEARDWEAPON) iCount += 5; - if (iInterruptConditions & COND_HEARDVOICE) iCount += 10; - - new bool:bDiscardMasterPos = bool:(GetGameTime() >= g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex]); - - if (GetVectorDistance(g_flSlenderTargetSoundTempPos[iBossIndex], g_flSlenderTargetSoundMasterPos[iBossIndex]) <= GetProfileFloat(sSlenderProfile, "search_sound_pos_dist_tolerance", 512.0) || - bDiscardMasterPos) - { - if (bDiscardMasterPos) g_iSlenderTargetSoundCount[iBossIndex] = 0; - - g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_sound_pos_discard_time", 2.0); - g_flSlenderTargetSoundMasterPos[iBossIndex][0] = g_flSlenderTargetSoundTempPos[iBossIndex][0]; - g_flSlenderTargetSoundMasterPos[iBossIndex][1] = g_flSlenderTargetSoundTempPos[iBossIndex][1]; - g_flSlenderTargetSoundMasterPos[iBossIndex][2] = g_flSlenderTargetSoundTempPos[iBossIndex][2]; - g_iSlenderTargetSoundCount[iBossIndex] += iCount; - } - - if (g_iSlenderTargetSoundCount[iBossIndex] >= GetProfileNum(sSlenderProfile, "search_sound_count_until_alert", 4)) - { - // Someone's making some noise over there! Time to investigate. - g_bSlenderInvestigatingSound[iBossIndex] = true; // This is just so that our sound position would be the goal position. - iState = STATE_ALERT; - } - } - } - case STATE_ALERT: - { - if (GetArraySize(g_hSlenderPath[iBossIndex]) <= 0) - { - // Fully navigated through our path. - iState = STATE_IDLE; - } - else if (GetGameTime() >= g_flSlenderTimeUntilIdle[iBossIndex]) - { - iState = STATE_IDLE; - } - else if (IsValidClient(iBestNewTarget)) - { - if (GetGameTime() >= g_flSlenderTimeUntilChase[iBossIndex] || bPlayerNear[iBestNewTarget]) - { - decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; - NPCGetEyePosition(iBossIndex, flTraceStartPos); - - if (IsValidClient(iBestNewTarget)) GetClientEyePosition(iBestNewTarget, flTraceEndPos); - else - { - decl Float:flTargetMins[3], Float:flTargetMaxs[3]; - GetEntPropVector(iBestNewTarget, Prop_Send, "m_vecMins", flTargetMins); - GetEntPropVector(iBestNewTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); - GetEntPropVector(iBestNewTarget, Prop_Data, "m_vecAbsOrigin", flTraceEndPos); - for (new i = 0; i < 3; i++) flTraceEndPos[i] += ((flTargetMins[i] + flTargetMaxs[i]) / 2.0); - } - - new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flTraceEndPos, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayBossVisibility, - slender); - - new bool:bIsVisible = !TR_DidHit(hTrace); - new iTraceHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (!bIsVisible && iTraceHitEntity == iBestNewTarget) bIsVisible = true; - - if ((bPlayerNear[iBestNewTarget] || bPlayerInFOV[iBestNewTarget]) && bPlayerVisible[iBestNewTarget]) - { - // AHAHAHAH! I GOT YOU NOW! - iTarget = iBestNewTarget; - g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); - iState = STATE_CHASE; - } - } - } - else - { - if (iInterruptConditions & COND_SAWENEMY) - { - if (IsValidClient(iBestNewTarget)) - { - g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][0]; - g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][1]; - g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][2]; - - bQueueForNewPath = true; - } - } - else if (iInterruptConditions & COND_HEARDSUSPICIOUSSOUND) - { - new bool:bDiscardMasterPos = bool:(GetGameTime() >= g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex]); - - if (GetVectorDistance(g_flSlenderTargetSoundTempPos[iBossIndex], g_flSlenderTargetSoundMasterPos[iBossIndex]) <= GetProfileFloat(sSlenderProfile, "search_sound_pos_dist_tolerance", 512.0) || - bDiscardMasterPos) - { - g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_sound_pos_discard_time", 2.0); - g_flSlenderTargetSoundMasterPos[iBossIndex][0] = g_flSlenderTargetSoundTempPos[iBossIndex][0]; - g_flSlenderTargetSoundMasterPos[iBossIndex][1] = g_flSlenderTargetSoundTempPos[iBossIndex][1]; - g_flSlenderTargetSoundMasterPos[iBossIndex][2] = g_flSlenderTargetSoundTempPos[iBossIndex][2]; - - // We have to manually set the goal position here because the goal position will not be changed due to no change in state. - g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderTargetSoundMasterPos[iBossIndex][0]; - g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderTargetSoundMasterPos[iBossIndex][1]; - g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderTargetSoundMasterPos[iBossIndex][2]; - - g_bSlenderInvestigatingSound[iBossIndex] = true; - - bQueueForNewPath = true; - } - } - - new bool:bBlockingProp = false; - - if (NPCGetFlags(iBossIndex) & SFF_ATTACKPROPS) - { - new prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bBlockingProp = true; - break; - } - } - - if (!bBlockingProp) - { - prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) - { - if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bBlockingProp = true; - break; - } - } - } - } - } - - if (bBlockingProp) - { - iState = STATE_ATTACK; - } - } - } - case STATE_CHASE, STATE_ATTACK, STATE_STUN: - { - if (iState == STATE_CHASE) - { - if (IsValidEdict(iTarget)) - { - decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; - NPCGetEyePosition(iBossIndex, flTraceStartPos); - - if (IsValidClient(iTarget)) - { - GetClientEyePosition(iTarget, flTraceEndPos); - } - else - { - decl Float:flTargetMins[3], Float:flTargetMaxs[3]; - GetEntPropVector(iTarget, Prop_Send, "m_vecMins", flTargetMins); - GetEntPropVector(iTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTraceEndPos); - for (new i = 0; i < 3; i++) flTraceEndPos[i] += ((flTargetMins[i] + flTargetMaxs[i]) / 2.0); - } - - new bool:bIsDeathPosVisible = false; - - if (g_bSlenderChaseDeathPosition[iBossIndex]) - { - new Handle:hTrace = TR_TraceRayFilterEx(flTraceStartPos, - g_flSlenderChaseDeathPosition[iBossIndex], - MASK_NPCSOLID, - RayType_EndPoint, - TraceRayBossVisibility, - slender); - bIsDeathPosVisible = !TR_DidHit(hTrace); - CloseHandle(hTrace); - } - - if (!bPlayerVisible[iTarget]) - { - if (GetArraySize(g_hSlenderPath[iBossIndex]) == 0) - { - iState = STATE_IDLE; - } - else if (GetGameTime() >= g_flSlenderTimeUntilAlert[iBossIndex]) - { - iState = STATE_ALERT; - } - else if (bIsDeathPosVisible) - { - iState = STATE_IDLE; - } - else if (iInterruptConditions & COND_CHASETARGETINVALIDATED) - { - if (!g_bSlenderChaseDeathPosition[iBossIndex]) - { - g_bSlenderChaseDeathPosition[iBossIndex] = true; - } - } - } - else - { - g_bSlenderChaseDeathPosition[iBossIndex] = false; // We're not chasing a dead player after all! Reset. - - decl Float:flAttackDirection[3]; - GetClientAbsOrigin(iTarget, g_flSlenderGoalPos[iBossIndex]); - SubtractVectors(g_flSlenderGoalPos[iBossIndex], flMyPos, flAttackDirection); - GetVectorAngles(flAttackDirection, flAttackDirection); - - if (GetVectorDistance(g_flSlenderGoalPos[iBossIndex], flMyPos) <= flAttackBeginRange && - (FloatAbs(AngleDiff(flAttackDirection[0], flMyEyeAng[0])) + FloatAbs(AngleDiff(flAttackDirection[1], flMyEyeAng[1]))) <= flAttackBeginFOV / 2.0) - { - // ENOUGH TALK! HAVE AT YOU! - iState = STATE_ATTACK; - } - else - { - new bool:bBlockingProp = false; - - if (NPCGetFlags(iBossIndex) & SFF_ATTACKPROPS) - { - new prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bBlockingProp = true; - break; - } - } - - if (!bBlockingProp) - { - prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) - { - if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bBlockingProp = true; - break; - } - } - } - } - } - - if (bBlockingProp) - { - iState = STATE_ATTACK; - } - else if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) - { - g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; - bQueueForNewPath = true; - } - } - } - } - else - { - // Even if the target isn't valid anymore, see if I still have some ways to go on my current path, - // because I shouldn't actually know that the target has died until I see it. - if (GetArraySize(g_hSlenderPath[iBossIndex]) == 0) - { - iState = STATE_IDLE; - } - } - } - else if (iState == STATE_ATTACK) - { - if (!g_bSlenderAttacking[iBossIndex]) - { - if (IsValidClient(iTarget)) - { - g_bSlenderChaseDeathPosition[iBossIndex] = false; - - // Chase him again! - iState = STATE_CHASE; - } - else - { - // Target isn't valid anymore. We killed him, Mac! - iState = STATE_ALERT; - } - } - } - else if (iState == STATE_STUN) - { - if (GetGameTime() >= g_flSlenderTimeUntilRecover[iBossIndex]) - { - NPCChaserSetStunHealth(iBossIndex, NPCChaserGetStunInitialHealth(iBossIndex)); - - if (IsValidClient(iTarget)) - { - // Chase him again! - iState = STATE_CHASE; - } - else - { - // WHAT DA FUUUUUUUUUUUQ. TARGET ISN'T VALID. AUSDHASUIHD - iState = STATE_ALERT; - } - } - } - } - } - - new bool:bDoChasePersistencyInit = false; - - if (iState != STATE_STUN) - { - if (bStunEnabled) - { - if (NPCChaserGetStunHealth(iBossIndex) <= 0) - { - if (iState != STATE_CHASE && iState != STATE_ATTACK) - { - // Sometimes players can stun the boss while it's not in chase mode. If that happens, we - // need to set the persistency value to the chase initial value. - bDoChasePersistencyInit = true; - } - - iState = STATE_STUN; - } - } - } - - // Finally, set our new state. - g_iSlenderState[iBossIndex] = iState; - - decl String:sAnimation[64]; - new iModel = EntRefToEntIndex(g_iSlenderModel[iBossIndex]); - - new Float:flPlaybackRateWalk = g_flSlenderWalkAnimationPlaybackRate[iBossIndex]; - new Float:flPlaybackRateRun = g_flSlenderRunAnimationPlaybackRate[iBossIndex]; - new Float:flPlaybackRateIdle = g_flSlenderIdleAnimationPlaybackRate[iBossIndex]; - - if (iOldState != iState) - { - switch (iState) - { - case STATE_IDLE, STATE_WANDER: - { - g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; - g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; - g_flSlenderTimeUntilChase[iBossIndex] = -1.0; - g_bSlenderChaseDeathPosition[iBossIndex] = false; - - if (iOldState != STATE_IDLE && iOldState != STATE_WANDER) - { - g_iSlenderTargetSoundCount[iBossIndex] = 0; - g_bSlenderInvestigatingSound[iBossIndex] = false; - g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = -1.0; - - g_flSlenderTimeUntilKill[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "idle_lifetime", 10.0); - } - - if (iState == STATE_WANDER) - { - // Force new wander position. - g_flSlenderNextWanderPos[iBossIndex] = -1.0; - } - - // Animation handling. - if (iModel && iModel != INVALID_ENT_REFERENCE) - { - if (iState == STATE_WANDER && (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE)) - { - if (GetProfileString(sSlenderProfile, "animation_walk", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateWalk); - } - } - else - { - if (GetProfileString(sSlenderProfile, "animation_idle", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, flPlaybackRateIdle); - } - } - } - } - - case STATE_ALERT: - { - g_bSlenderChaseDeathPosition[iBossIndex] = false; - - // Set our goal position. - if (g_bSlenderInvestigatingSound[iBossIndex]) - { - g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderTargetSoundMasterPos[iBossIndex][0]; - g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderTargetSoundMasterPos[iBossIndex][1]; - g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderTargetSoundMasterPos[iBossIndex][2]; - } - - g_flSlenderTimeUntilIdle[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_alert_duration", 5.0); - g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; - g_flSlenderTimeUntilChase[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_alert_gracetime", 0.5); - - bQueueForNewPath = true; - - // Animation handling. - if (iModel && iModel != INVALID_ENT_REFERENCE) - { - if (GetProfileString(sSlenderProfile, "animation_walk", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateWalk); - } - } - } - case STATE_CHASE, STATE_ATTACK, STATE_STUN: - { - g_bSlenderInvestigatingSound[iBossIndex] = false; - g_iSlenderTargetSoundCount[iBossIndex] = 0; - - if (iOldState != STATE_ATTACK && iOldState != STATE_CHASE && iOldState != STATE_STUN) - { - g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; - g_flSlenderTimeUntilAlert[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration", 10.0); - g_flSlenderTimeUntilChase[iBossIndex] = -1.0; - - new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init", 5.0); - if (flPersistencyTime >= 0.0) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; - } - } - - if (iState == STATE_ATTACK) - { - g_bSlenderAttacking[iBossIndex] = true; - g_hSlenderAttackTimer[iBossIndex] = CreateTimer(NPCChaserGetAttackDamageDelay(iBossIndex, 0), Timer_SlenderChaseBossAttack, EntIndexToEntRef(slender), TIMER_FLAG_NO_MAPCHANGE); - - new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_attack", -1.0); - if (flPersistencyTime >= 0.0) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; - } - - flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_attack", 2.0); - if (flPersistencyTime >= 0.0) - { - if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); - g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; - } - - SlenderPerformVoice(iBossIndex, "sound_attackenemy"); - } - else if (iState == STATE_STUN) - { - if (g_bSlenderAttacking[iBossIndex]) - { - // Cancel attacking. - g_bSlenderAttacking[iBossIndex] = false; - g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; - } - - if (!bDoChasePersistencyInit) - { - new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_stun", -1.0); - if (flPersistencyTime >= 0.0) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; - } - - flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_stun", 2.0); - if (flPersistencyTime >= 0.0) - { - if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); - g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; - } - } - else - { - new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init", 5.0); - if (flPersistencyTime >= 0.0) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; - } - } - - g_flSlenderTimeUntilRecover[iBossIndex] = GetGameTime() + NPCChaserGetStunDuration(iBossIndex); - - // Sound handling. Ignore time check. - SlenderPerformVoice(iBossIndex, "sound_stun"); - } - else - { - if (iOldState != STATE_ATTACK) - { - // Sound handling. - SlenderPerformVoice(iBossIndex, "sound_chaseenemyinitial"); - } - } - - // Animation handling. - if (iModel && iModel != INVALID_ENT_REFERENCE) - { - if (iState == STATE_CHASE) - { - if (GetProfileString(sSlenderProfile, "animation_run", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateRun); - } - } - else if (iState == STATE_ATTACK) - { - if (GetProfileString(sSlenderProfile, "animation_attack", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, GetProfileFloat(sSlenderProfile, "animation_attack_playbackrate", 1.0)); - } - } - else if (iState == STATE_STUN) - { - if (GetProfileString(sSlenderProfile, "animation_stun", sAnimation, sizeof(sAnimation))) - { - EntitySetAnimation(iModel, sAnimation, _, GetProfileFloat(sSlenderProfile, "animation_stun_playbackrate", 1.0)); - } - } - } - } - } - - // Call our forward. - Call_StartForward(fOnBossChangeState); - Call_PushCell(iBossIndex); - Call_PushCell(iOldState); - Call_PushCell(iState); - Call_Finish(); - } - - switch (iState) - { - case STATE_IDLE: - { - // Animation playback speed handling. - if (iModel && iModel != INVALID_ENT_REFERENCE) - { - SetVariantFloat(flPlaybackRateIdle); - AcceptEntityInput(iModel, "SetPlaybackRate"); - } - } - case STATE_WANDER, STATE_ALERT, STATE_CHASE, STATE_ATTACK: - { - // These deal with movement, therefore we need to set our - // destination first. That is, if we don't have one. (nav mesh only) - - if (iState == STATE_WANDER) - { - if (GetGameTime() >= g_flSlenderNextWanderPos[iBossIndex]) - { - new Float:flMin = GetProfileFloat(sSlenderProfile, "search_wander_time_min", 4.0); - new Float:flMax = GetProfileFloat(sSlenderProfile, "search_wander_time_max", 6.5); - g_flSlenderNextWanderPos[iBossIndex] = GetGameTime() + GetRandomFloat(flMin, flMax); - - if (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE) - { - // We're allowed to move in wander mode. Get a new wandering position and create a path to follow. - // If the position can't be reached, then just get to the closest area that we can get. - new Float:flWanderRangeMin = GetProfileFloat(sSlenderProfile, "search_wander_range_min", 400.0); - new Float:flWanderRangeMax = GetProfileFloat(sSlenderProfile, "search_wander_range_max", 1024.0); - new Float:flWanderRange = GetRandomFloat(flWanderRangeMin, flWanderRangeMax); - - decl Float:flWanderPos[3]; - flWanderPos[0] = 0.0; - flWanderPos[1] = GetRandomFloat(0.0, 360.0); - flWanderPos[2] = 0.0; - - GetAngleVectors(flWanderPos, flWanderPos, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flWanderPos, flWanderPos); - ScaleVector(flWanderPos, flWanderRange); - AddVectors(flWanderPos, flMyPos, flWanderPos); - - g_flSlenderGoalPos[iBossIndex][0] = flWanderPos[0]; - g_flSlenderGoalPos[iBossIndex][1] = flWanderPos[1]; - g_flSlenderGoalPos[iBossIndex][2] = flWanderPos[2]; - - bQueueForNewPath = true; - g_flSlenderNextPathTime[iBossIndex] = -1.0; // We're not going to wander around too much, so no need for a time constraint. - } - } - } - else if (iState == STATE_ALERT) - { - if (iInterruptConditions & COND_SAWENEMY) - { - if (IsValidEntity(iBestNewTarget)) - { - if ((bPlayerInFOV[iBestNewTarget] || bPlayerNear[iBestNewTarget]) && bPlayerVisible[iBestNewTarget]) - { - // Constantly update my path if I see him. - if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) - { - GetEntPropVector(iBestNewTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); - bQueueForNewPath = true; - g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; - } - } - } - } - } - else if (iState == STATE_CHASE || iState == STATE_ATTACK) - { - if (IsValidEntity(iBestNewTarget)) - { - iOldTarget = iTarget; - iTarget = iBestNewTarget; - g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); - } - - if (iTarget != INVALID_ENT_REFERENCE) - { - if (iOldTarget != iTarget) - { - // Brand new target! We need a path, and we need to reset our persistency, if needed. - new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_newtarget", -1.0); - if (flPersistencyTime >= 0.0) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; - } - - flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_newtarget", 2.0); - if (flPersistencyTime >= 0.0) - { - if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); - g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; - } - - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); - bQueueForNewPath = true; // Brand new target! We need a new path! - } - else if ((bPlayerInFOV[iTarget] && bPlayerVisible[iTarget]) || GetGameTime() < g_flSlenderTimeUntilNoPersistence[iBossIndex]) - { - // Constantly update my path if I see him or if I'm still being persistent. - if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) - { - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); - bQueueForNewPath = true; - g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; - } - } - } - } - - if (NavMesh_Exists()) - { - // So by now we should have calculated our master goal position. - // Now we use that to create a path. - - if (bQueueForNewPath) - { - ClearArray(g_hSlenderPath[iBossIndex]); - - new iCurrentAreaIndex = NavMesh_GetNearestArea(flMyPos); - if (iCurrentAreaIndex != -1) - { - new iGoalAreaIndex = NavMesh_GetNearestArea(g_flSlenderGoalPos[iBossIndex]); - if (iGoalAreaIndex != -1) - { - decl Float:flCenter[3], Float:flCenterPortal[3], Float:flClosestPoint[3]; - new iClosestAreaIndex = 0; - - new bool:bPathSuccess = NavMesh_BuildPath(iCurrentAreaIndex, - iGoalAreaIndex, - g_flSlenderGoalPos[iBossIndex], - SlenderChaseBossShortestPathCost, - RoundToFloor(NPCChaserGetStepSize(iBossIndex)), - iClosestAreaIndex); - - new iTempAreaIndex = iClosestAreaIndex; - new iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); - new iNavDirection; - new Float:flHalfWidth; - - if (bPathSuccess) - { - // Path successful? Insert the goal position into our list. - new iIndex = PushArrayCell(g_hSlenderPath[iBossIndex], g_flSlenderGoalPos[iBossIndex][0]); - SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, g_flSlenderGoalPos[iBossIndex][1], 1); - SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, g_flSlenderGoalPos[iBossIndex][2], 2); - } - - while (iTempParentAreaIndex != -1) - { - // Build a path of waypoints along the nav mesh for our AI to follow. - // Path order is first come, first served, so when we got our waypoint list, - // we have to reverse it so that the starting waypoint would be in front. - - NavMeshArea_GetCenter(iTempParentAreaIndex, flCenter); - iNavDirection = NavMeshArea_ComputeDirection(iTempAreaIndex, flCenter); - NavMeshArea_ComputePortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flHalfWidth); - NavMeshArea_ComputeClosestPointInPortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flClosestPoint); - - flClosestPoint[2] = NavMeshArea_GetZ(iTempAreaIndex, flClosestPoint); - - new iIndex = PushArrayCell(g_hSlenderPath[iBossIndex], flClosestPoint[0]); - SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, flClosestPoint[1], 1); - SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, flClosestPoint[2], 2); - - iTempAreaIndex = iTempParentAreaIndex; - iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); - } - - // Set our goal position to the start node (hopefully there's something in the array). - if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) - { - new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; - - g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); - g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); - g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); - } - } - else - { - PrintToServer("SF2: Failed to create new path for boss %d: destination is not on nav mesh!", iBossIndex); - } - } - else - { - PrintToServer("SF2: Failed to create new path for boss %d: boss is not on nav mesh!", iBossIndex); - } - } - } - else - { - // The nav mesh doesn't exist? Well, that sucks. - ClearArray(g_hSlenderPath[iBossIndex]); - } - - if (iState == STATE_CHASE || iState == STATE_ATTACK) - { - if (IsValidClient(iTarget)) - { -#if defined DEBUG - SendDebugMessageToPlayer(iTarget, DEBUG_BOSS_CHASE, 1, "g_flSlenderTimeUntilAlert[%d]: %f\ng_flSlenderTimeUntilNoPersistence[%d]: %f", iBossIndex, g_flSlenderTimeUntilAlert[iBossIndex] - GetGameTime(), iBossIndex, g_flSlenderTimeUntilNoPersistence[iBossIndex] - GetGameTime()); -#endif - - if (bPlayerInFOV[iTarget] && bPlayerVisible[iTarget]) - { - new Float:flDistRatio = flPlayerDists[iTarget] / NPCGetSearchRadius(iBossIndex); - - new Float:flChaseDurationTimeAddMin = GetProfileFloat(sSlenderProfile, "search_chase_duration_add_visible_min", 0.025); - new Float:flChaseDurationTimeAddMax = GetProfileFloat(sSlenderProfile, "search_chase_duration_add_visible_max", 0.2); - - new Float:flChaseDurationAdd = flChaseDurationTimeAddMax - ((flChaseDurationTimeAddMax - flChaseDurationTimeAddMin) * flDistRatio); - - if (flChaseDurationAdd > 0.0) - { - g_flSlenderTimeUntilAlert[iBossIndex] += flChaseDurationAdd; - if (g_flSlenderTimeUntilAlert[iBossIndex] > (GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"))) - { - g_flSlenderTimeUntilAlert[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"); - } - } - - new Float:flPersistencyTimeAddMin = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_visible_min", 0.05); - new Float:flPersistencyTimeAddMax = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_visible_max", 0.15); - - new Float:flPersistencyTimeAdd = flPersistencyTimeAddMax - ((flPersistencyTimeAddMax - flPersistencyTimeAddMin) * flDistRatio); - - if (flPersistencyTimeAdd > 0.0) - { - if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); - - g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTimeAdd; - if (g_flSlenderTimeUntilNoPersistence[iBossIndex] > (GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"))) - { - g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"); - } - } - } - } - } - - // Process through our path waypoints. - if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) - { - decl Float:flHitNormal[3]; - decl Float:flNodePos[3]; - - new Float:flNodeToleranceDist = g_flSlenderPathNodeTolerance[iBossIndex]; - new bool:bGotNewPoint = false; - - for (new iNodeIndex = 0, iNodeCount = GetArraySize(g_hSlenderPath[iBossIndex]); iNodeIndex < iNodeCount; iNodeIndex++) - { - flNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 0); - flNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 1); - flNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 2); - - new Handle:hTrace = TR_TraceHullFilterEx(flMyPos, - flNodePos, - flSlenderMins, - flSlenderMaxs, - MASK_NPCSOLID, - TraceRayDontHitCharactersOrEntity, - slender); - - new bool:bDidHit = TR_DidHit(hTrace); - TR_GetPlaneNormal(hTrace, flHitNormal); - CloseHandle(hTrace); - GetVectorAngles(flHitNormal, flHitNormal); - for (new i = 0; i < 3; i++) flHitNormal[i] = AngleNormalize(flHitNormal[i]); - - // First check if we can see the point. - if (!bDidHit || ((flHitNormal[0] >= 0.0 && flHitNormal[0] > 45.0) || (flHitNormal[0] < 0.0 && flHitNormal[0] < -45.0))) - { - new bool:bNearNode = false; - - // See if we're already near enough. - new Float:flDist = GetVectorDistance(flNodePos, flMyPos); - if (flDist < flNodeToleranceDist) bNearNode = true; - - if (!bNearNode) - { - new bool:bOutside = false; - - // Then, predict if we're going to pass over the point on the next think. - decl Float:flTestPos[3]; - NormalizeVector(flSlenderVelocity, flTestPos); - ScaleVector(flTestPos, GetVectorLength(flSlenderVelocity) * BOSS_THINKRATE); - AddVectors(flMyPos, flTestPos, flTestPos); - - decl Float:flP[3], Float:flS[3]; - SubtractVectors(flNodePos, flMyPos, flP); - SubtractVectors(flTestPos, flMyPos, flS); - - new Float:flSP = GetVectorDotProduct(flP, flS); - if (flSP <= 0.0) bOutside = true; - - new Float:flPP = GetVectorDotProduct(flS, flS); - - if (!bOutside) - { - if (flPP <= flSP) bOutside = true; - } - - if (!bOutside) - { - decl Float:flD[3]; - ScaleVector(flS, (flSP / flPP)); - SubtractVectors(flP, flS, flD); - - flDist = GetVectorLength(flD); - if (flDist < flNodeToleranceDist) - { - bNearNode = true; - } - } - } - - if (bNearNode) - { - // Shave off this node and set our goal position to the next one. - - ResizeArray(g_hSlenderPath[iBossIndex], iNodeIndex); - - if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) - { - new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; - - g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); - g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); - g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); - } - - bGotNewPoint = true; - break; - } - } - } - - if (!bGotNewPoint) - { - // Try to see if we can look ahead. - - decl Float:flMyEyePos[3]; - NPCGetEyePosition(iBossIndex, flMyEyePos); - - new Float:flNodeLookAheadDist = g_flSlenderPathNodeLookAhead[iBossIndex]; - if (flNodeLookAheadDist > 0.0) - { - new iNodeCount = GetArraySize(g_hSlenderPath[iBossIndex]); - if (iNodeCount) - { - decl Float:flInitDir[3]; - flInitDir[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 0); - flInitDir[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 1); - flInitDir[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 2); - - SubtractVectors(flInitDir, flMyPos, flInitDir); - NormalizeVector(flInitDir, flInitDir); - - decl Float:flPrevDir[3]; - flPrevDir[0] = flInitDir[0]; - flPrevDir[1] = flInitDir[1]; - flPrevDir[2] = flInitDir[2]; - - NormalizeVector(flPrevDir, flPrevDir); - - decl Float:flPrevNodePos[3]; - - new iStartPointIndex = iNodeCount - 1; - new Float:flRangeSoFar = 0.0; - - new iLookAheadPointIndex; - for (iLookAheadPointIndex = iStartPointIndex; iLookAheadPointIndex >= 0; iLookAheadPointIndex--) - { - flNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 0); - flNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 1); - flNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 2); - - decl Float:flDir[3]; - if (iLookAheadPointIndex == iStartPointIndex) - { - SubtractVectors(flNodePos, flMyPos, flDir); - NormalizeVector(flDir, flDir); - } - else - { - flPrevNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 0); - flPrevNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 1); - flPrevNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 2); - - SubtractVectors(flNodePos, flPrevNodePos, flDir); - NormalizeVector(flDir, flDir); - } - - if (GetVectorDotProduct(flDir, flInitDir) < 0.0) - { - break; - } - - if (GetVectorDotProduct(flDir, flPrevDir) < 0.5) - { - break; - } - - flPrevDir[0] = flDir[0]; - flPrevDir[1] = flDir[1]; - flPrevDir[2] = flDir[2]; - - decl Float:flProbe[3]; - flProbe[0] = flNodePos[0]; - flProbe[1] = flNodePos[1]; - flProbe[2] = flNodePos[2] + HalfHumanHeight; - - if (!IsWalkableTraceLineClear(flMyEyePos, flProbe, WALK_THRU_BREAKABLES)) - { - break; - } - - if (iLookAheadPointIndex == iStartPointIndex) - { - flRangeSoFar += GetVectorDistance(flMyPos, flNodePos); - } - else - { - flRangeSoFar += GetVectorDistance(flNodePos, flPrevNodePos); - } - - if (flRangeSoFar >= flNodeLookAheadDist) - { - break; - } - } - - // Shave off all unnecessary nodes and keep the one that is within - // our viewsight. - - ResizeArray(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1); - - if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) - { - new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; - - g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); - g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); - g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); - } - - bGotNewPoint = true; - } - } - } - } - - if (iState != STATE_ATTACK && iState != STATE_STUN) - { - // Animation playback speed handling. - if (iModel && iModel != INVALID_ENT_REFERENCE) - { - if (iState == STATE_WANDER && !(NPCGetFlags(iBossIndex) & SFF_WANDERMOVE)) - { - SetVariantFloat(flPlaybackRateIdle); - AcceptEntityInput(iModel, "SetPlaybackRate"); - } - else - { - SetVariantFloat(iState == STATE_CHASE ? (flVelocityRatio * flPlaybackRateRun) : (flVelocityRatioWalk * flPlaybackRateWalk)); - AcceptEntityInput(iModel, "SetPlaybackRate"); - } - } - } - } - } - - // Sound handling. - if (GetGameTime() >= g_flSlenderNextVoiceSound[iBossIndex]) - { - if (iState == STATE_IDLE || iState == STATE_WANDER) - { - SlenderPerformVoice(iBossIndex, "sound_idle"); - } - else if (iState == STATE_ALERT) - { - SlenderPerformVoice(iBossIndex, "sound_alertofenemy"); - } - else if (iState == STATE_CHASE || iState == STATE_ATTACK) - { - SlenderPerformVoice(iBossIndex, "sound_chasingenemy"); - } - } - - // Reset our interrupt conditions. - g_iSlenderInterruptConditions[iBossIndex] = 0; - - return Plugin_Continue; -} - -SlenderChaseBossProcessMovement(iBossIndex) -{ - new iBoss = NPCGetEntIndex(iBossIndex); - new iState = g_iSlenderState[iBossIndex]; - - // Constantly set the monster_generic's NPC state to idle to prevent - // velocity confliction. - - SetEntProp(iBoss, Prop_Data, "m_NPCState", 0); - - new Float:flWalkSpeed = g_flSlenderCalculatedWalkSpeed[iBossIndex]; - new Float:flSpeed = g_flSlenderCalculatedSpeed[iBossIndex]; - - new Float:flMyPos[3], Float:flMyEyeAng[3], Float:flMyVelocity[3]; - - decl String:sSlenderProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sSlenderProfile, sizeof(sSlenderProfile)); - - GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flMyPos); - GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); - GetEntPropVector(iBoss, Prop_Data, "m_vecAbsVelocity", flMyVelocity); - - decl Float:flBossMins[3], Float:flBossMaxs[3]; - GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flBossMins); - GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flBossMaxs); - - decl Float:flTraceMins[3], Float:flTraceMaxs[3]; - flTraceMins[0] = flBossMins[0]; - flTraceMins[1] = flBossMins[1]; - flTraceMins[2] = 0.0; - flTraceMaxs[0] = flBossMaxs[0]; - flTraceMaxs[1] = flBossMaxs[1]; - flTraceMaxs[2] = 0.0; - - // By now we should have our preferable goal position. Initiate - // reflex adjustments. - - g_bSlenderFeelerReflexAdjustment[iBossIndex] = false; - - { - decl Float:flMoveDir[3]; - NormalizeVector(flMyVelocity, flMoveDir); - flMoveDir[2] = 0.0; - - decl Float:flLat[3]; - flLat[0] = -flMoveDir[1]; - flLat[1] = flMoveDir[0]; - flLat[2] = 0.0; - - new Float:flFeelerOffset = 25.0; - new Float:flFeelerLengthRun = 50.0; - new Float:flFeelerLengthWalk = 30.0; - new Float:flFeelerHeight = StepHeight + 0.1; - - new Float:flFeelerLength = iState == STATE_CHASE ? flFeelerLengthRun : flFeelerLengthWalk; - - // Get the ground height and normal. - new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, Float:{ 0.0, 0.0, 90.0 }, MASK_NPCSOLID, RayType_Infinite, TraceFilterWalkableEntities); - decl Float:flTraceEndPos[3]; - decl Float:flTraceNormal[3]; - TR_GetEndPosition(flTraceEndPos, hTrace); - TR_GetPlaneNormal(hTrace, flTraceNormal); - new bool:bTraceHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bTraceHit) - { - //new Float:flGroundHeight = GetVectorDistance(flMyPos, flTraceEndPos); - NormalizeVector(flTraceNormal, flTraceNormal); - GetVectorCrossProduct(flLat, flTraceNormal, flMoveDir); - GetVectorCrossProduct(flMoveDir, flTraceNormal, flLat); - - decl Float:flFeet[3]; - flFeet[0] = flMyPos[0]; - flFeet[1] = flMyPos[1]; - flFeet[2] = flMyPos[2] + flFeelerHeight; - - decl Float:flTo[3]; - decl Float:flFrom[3]; - for (new i = 0; i < 3; i++) - { - flFrom[i] = flFeet[i] + (flFeelerOffset * flLat[i]); - flTo[i] = flFrom[i] + (flFeelerLength * flMoveDir[i]); - } - - new bool:bLeftClear = IsWalkableTraceLineClear(flFrom, flTo, WALK_THRU_DOORS | WALK_THRU_BREAKABLES); - - for (new i = 0; i < 3; i++) - { - flFrom[i] = flFeet[i] - (flFeelerOffset * flLat[i]); - flTo[i] = flFrom[i] + (flFeelerLength * flMoveDir[i]); - } - - new bool:bRightClear = IsWalkableTraceLineClear(flFrom, flTo, WALK_THRU_DOORS | WALK_THRU_BREAKABLES); - - new Float:flAvoidRange = 300.0; - - if (!bRightClear) - { - if (bLeftClear) - { - g_bSlenderFeelerReflexAdjustment[iBossIndex] = true; - - for (new i = 0; i < 3; i++) - { - g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i] = g_flSlenderGoalPos[iBossIndex][i] + (flAvoidRange * flLat[i]); - } - } - } - else if (!bLeftClear) - { - g_bSlenderFeelerReflexAdjustment[iBossIndex] = true; - - for (new i = 0; i < 3; i++) - { - g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i] = g_flSlenderGoalPos[iBossIndex][i] - (flAvoidRange * flLat[i]); - } - } - } - } - - new Float:flGoalPosition[3]; - if (g_bSlenderFeelerReflexAdjustment[iBossIndex]) - { - for (new i = 0; i < 3; i++) - { - flGoalPosition[i] = g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i]; - } - } - else - { - for (new i = 0; i < 3; i++) - { - flGoalPosition[i] = g_flSlenderGoalPos[iBossIndex][i]; - } - } - - // Process our desired velocity. - new Float:flDesiredVelocity[3]; - switch (iState) - { - case STATE_WANDER: - { - if (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE) - { - SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); - flDesiredVelocity[2] = 0.0; - NormalizeVector(flDesiredVelocity, flDesiredVelocity); - ScaleVector(flDesiredVelocity, flWalkSpeed); - } - } - case STATE_ALERT: - { - SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); - flDesiredVelocity[2] = 0.0; - NormalizeVector(flDesiredVelocity, flDesiredVelocity); - ScaleVector(flDesiredVelocity, flWalkSpeed); - } - case STATE_CHASE: - { - SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); - flDesiredVelocity[2] = 0.0; - NormalizeVector(flDesiredVelocity, flDesiredVelocity); - ScaleVector(flDesiredVelocity, flSpeed); - } - } - - // Check if we're on the ground. - new bool:bSlenderOnGround = bool:(GetEntityFlags(iBoss) & FL_ONGROUND); - - decl Float:flTraceEndPos[3]; - new Handle:hTrace; - - // Determine speed behavior. - if (bSlenderOnGround) - { - // Don't change the speed behavior. - } - else - { - flDesiredVelocity[2] = 0.0; - NormalizeVector(flDesiredVelocity, flDesiredVelocity); - ScaleVector(flDesiredVelocity, NPCChaserGetAirSpeed(iBossIndex, GetConVarInt(g_cvDifficulty))); - } - - new bool:bSlenderTeleportedOnStep = false; - new Float:flSlenderStepSize = NPCChaserGetStepSize(iBossIndex); - - // Check our stepsize in case we need to elevate ourselves a step. - if (bSlenderOnGround && GetVectorLength(flDesiredVelocity) > 0.0) - { - if (flSlenderStepSize > 0.0) - { - decl Float:flTraceDirection[3], Float:flObstaclePos[3], Float:flObstacleNormal[3]; - NormalizeVector(flDesiredVelocity, flTraceDirection); - AddVectors(flMyPos, flTraceDirection, flTraceEndPos); - - // Tracehull in front of us to check if there's a very small obstacle blocking our way. - hTrace = TR_TraceHullFilterEx(flMyPos, - flTraceEndPos, - flBossMins, - flBossMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - new bool:bSlenderHitObstacle = TR_DidHit(hTrace); - TR_GetEndPosition(flObstaclePos, hTrace); - TR_GetPlaneNormal(hTrace, flObstacleNormal); - CloseHandle(hTrace); - - if (bSlenderHitObstacle && - FloatAbs(flObstacleNormal[2]) == 0.0) - { - decl Float:flTraceStartPos[3]; - flTraceStartPos[0] = flObstaclePos[0]; - flTraceStartPos[1] = flObstaclePos[1]; - - decl Float:flTraceFreePos[3]; - - new Float:flTraceCheckZ = 0.0; - - // This does a crapload of traces along the wall. Very nasty and expensive to do... - while (flTraceCheckZ <= flSlenderStepSize) - { - flTraceCheckZ += 1.0; - flTraceStartPos[2] = flObstaclePos[2] + flTraceCheckZ; - - AddVectors(flTraceStartPos, flTraceDirection, flTraceEndPos); - - hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flTraceEndPos, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - bSlenderHitObstacle = TR_DidHit(hTrace); - TR_GetEndPosition(flTraceFreePos, hTrace); - CloseHandle(hTrace); - - if (!bSlenderHitObstacle) - { - // Potential space to step on? See if we can fit! - if (!IsSpaceOccupiedNPC(flTraceFreePos, - flBossMins, - flBossMaxs, - iBoss)) - { - // Yes we can! Break the loop and teleport to this pos. - bSlenderTeleportedOnStep = true; - TeleportEntity(iBoss, flTraceFreePos, NULL_VECTOR, NULL_VECTOR); - break; - } - } - } - } - /* - else if (!bSlenderHitObstacle) - { - decl Float:flTraceStartPos[3]; - flTraceStartPos[0] = flObstaclePos[0]; - flTraceStartPos[1] = flObstaclePos[1]; - - decl Float:flTraceFreePos[3]; - - new Float:flTraceCheckZ = 0.0; - - // This does a crapload of traces along the wall. Very nasty and expensive to do... - while (flTraceCheckZ <= flSlenderStepSize) - { - flTraceCheckZ += 1.0; - flTraceStartPos[2] = flObstaclePos[2] - flTraceCheckZ; - - AddVectors(flTraceStartPos, flTraceDirection, flTraceEndPos); - - hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flTraceEndPos, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - bSlenderHitObstacle = TR_DidHit(hTrace); - TR_GetEndPosition(flTraceFreePos, hTrace); - CloseHandle(hTrace); - - if (bSlenderHitObstacle) - { - // Potential space to step on? See if we can fit! - if (!IsSpaceOccupiedNPC(flTraceFreePos, - flBossMins, - flBossMaxs, - iBoss)) - { - // Yes we can! Break the loop and teleport to this pos. - bSlenderTeleportedOnStep = true; - TeleportEntity(iBoss, flTraceFreePos, NULL_VECTOR, NULL_VECTOR); - break; - } - } - } - } - */ - } - } - - // Apply acceleration vectors. - new Float:flMoveVelocity[3]; - new Float:flFrameTime = GetTickInterval(); - decl Float:flAcceleration[3]; - SubtractVectors(flDesiredVelocity, flMyVelocity, flAcceleration); - NormalizeVector(flAcceleration, flAcceleration); - ScaleVector(flAcceleration, g_flSlenderAcceleration[iBossIndex] * flFrameTime); - - AddVectors(flMyVelocity, flAcceleration, flMoveVelocity); - - new Float:flSlenderJumpSpeed = g_flSlenderJumpSpeed[iBossIndex]; - new bool:bSlenderShouldJump = false; - - decl Float:angJumpReach[3]; - - // Check if we need to jump over a wall or something. - if (!bSlenderShouldJump && bSlenderOnGround && !bSlenderTeleportedOnStep && flSlenderJumpSpeed > 0.0 && GetVectorLength(flDesiredVelocity) > 0.0 && - GetGameTime() >= g_flSlenderNextJump[iBossIndex]) - { - new Float:flZDiff = (flGoalPosition[2] - flMyPos[2]); - - if (flZDiff > flSlenderStepSize) - { - // Our path has a jump thingy to it. Calculate the jump height needed to reach it and how far away we should start - // checking on when to jump. - - decl Float:vecDir[3], Float:vecDesiredDir[3]; - GetVectorAngles(flMyVelocity, vecDir); - SubtractVectors(flGoalPosition, flMyPos, vecDesiredDir); - GetVectorAngles(vecDesiredDir, vecDesiredDir); - - if ((FloatAbs(AngleDiff(vecDir[0], vecDesiredDir[0])) + FloatAbs(AngleDiff(vecDir[1], vecDesiredDir[1]))) >= 45.0) - { - // Assuming we are actually capable of making the jump, find out WHEN we have to jump, - // based on 2D distance between our position and the target point, and our current horizontal - // velocity. - - decl Float:vecMyPos2D[3], Float:vecGoalPos2D[3]; - vecMyPos2D[0] = flMyPos[0]; - vecMyPos2D[1] = flMyPos[1]; - vecMyPos2D[2] = 0.0; - vecGoalPos2D[0] = flGoalPosition[0]; - vecGoalPos2D[1] = flGoalPosition[1]; - vecGoalPos2D[2] = 0.0; - - new Float:fl2DDist = GetVectorDistance(vecMyPos2D, vecGoalPos2D); - - new Float:flNotImaginary = Pow(flSlenderJumpSpeed, 4.0) - (g_flGravity * (g_flGravity * Pow(fl2DDist, 2.0)) + (2.0 * flZDiff * Pow(flSlenderJumpSpeed, 2.0))); - if (flNotImaginary >= 0.0) - { - // We can reach it. - new Float:flNotInfinite = g_flGravity * fl2DDist; - if (flNotInfinite > 0.0) - { - SubtractVectors(vecGoalPos2D, vecMyPos2D, angJumpReach); - GetVectorAngles(angJumpReach, angJumpReach); - angJumpReach[0] = -RadToDeg(ArcTangent((Pow(flSlenderJumpSpeed, 2.0) + SquareRoot(flNotImaginary)) / flNotInfinite)); - bSlenderShouldJump = true; - } - } - } - } - } - - if (bSlenderOnGround && bSlenderShouldJump) - { - g_flSlenderNextJump[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "jump_cooldown", 2.0); - - decl Float:vecJump[3]; - GetAngleVectors(angJumpReach, vecJump, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(vecJump, vecJump); - ScaleVector(vecJump, flSlenderJumpSpeed); - AddVectors(flMoveVelocity, vecJump, flMoveVelocity); - } - else - { - // We are in no position to defy gravity. - flMoveVelocity[2] = flMyVelocity[2]; - } - - decl Float:flMoveAng[3]; - new bool:bChangeAngles = false; - - // Process angles. - if (iState != STATE_ATTACK && iState != STATE_STUN) - { - if (NPCHasAttribute(iBossIndex, "always look at target")) - { - new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); - - if (iTarget && iTarget != INVALID_ENT_REFERENCE) - { - decl Float:flTargetPos[3]; - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTargetPos); - SubtractVectors(flTargetPos, flMyPos, flMoveAng); - GetVectorAngles(flMoveAng, flMoveAng); - } - else - { - SubtractVectors(flGoalPosition, flMyPos, flMoveAng); - GetVectorAngles(flMoveAng, flMoveAng); - } - } - else - { - SubtractVectors(flGoalPosition, flMyPos, flMoveAng); - GetVectorAngles(flMoveAng, flMoveAng); - } - - new Float:flTurnRate = NPCGetTurnRate(iBossIndex); - if (iState == STATE_CHASE) flTurnRate *= 2.0; - - flMoveAng[0] = 0.0; - flMoveAng[2] = 0.0; - flMoveAng[1] = ApproachAngle(flMoveAng[1], flMyEyeAng[1], flTurnRate * flFrameTime); - - bChangeAngles = true; - } - - TeleportEntity(iBoss, NULL_VECTOR, bChangeAngles ? flMoveAng : NULL_VECTOR, flMoveVelocity); -} - -// Shortest-path cost function for NavMesh_BuildPath. -public SlenderChaseBossShortestPathCost(iAreaIndex, iFromAreaIndex, iLadderIndex, any:iStepSize) -{ - if (iFromAreaIndex == -1) - { - return 0; - } - else - { - new iDist; - decl Float:flAreaCenter[3], Float:flFromAreaCenter[3]; - NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); - NavMeshArea_GetCenter(iFromAreaIndex, flFromAreaCenter); - - if (iLadderIndex != -1) - { - iDist = RoundFloat(NavMeshLadder_GetLength(iLadderIndex)); - } - else - { - iDist = RoundFloat(GetVectorDistance(flAreaCenter, flFromAreaCenter)); - } - - new iCost = iDist + NavMeshArea_GetCostSoFar(iFromAreaIndex); - - new iAreaFlags = NavMeshArea_GetFlags(iAreaIndex); - if (iAreaFlags & NAV_MESH_CROUCH) iCost += 20; - if (iAreaFlags & NAV_MESH_JUMP) iCost += (5 * iDist); - - if ((flAreaCenter[2] - flFromAreaCenter[2]) > iStepSize) iCost += iStepSize; - - return iCost; - } -} - -public Action:Timer_SlenderChaseBossAttack(Handle:timer, any:entref) -{ - if (!g_bEnabled) return; - - new slender = EntRefToEntIndex(entref); - if (!slender || slender == INVALID_ENT_REFERENCE) return; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return; - - if (timer != g_hSlenderAttackTimer[iBossIndex]) return; - - if (NPCGetFlags(iBossIndex) & SFF_FAKE) - { - SlenderMarkAsFake(iBossIndex); - return; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new bool:bAttackEliminated = bool:(NPCGetFlags(iBossIndex) & SFF_ATTACKWAITERS); - - new Float:flDamage = NPCChaserGetAttackDamage(iBossIndex, 0); - new Float:flDamageVsProps = NPCChaserGetAttackDamageVsProps(iBossIndex, 0); - new iDamageType = NPCChaserGetAttackDamageType(iBossIndex, 0); - - // Damage all players within range. - decl Float:flMyEyePos[3], Float:flMyEyeAng[3]; - NPCGetEyePosition(iBossIndex, flMyEyePos); - GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", flMyEyeAng); - AddVectors(g_flSlenderEyePosOffset[iBossIndex], flMyEyeAng, flMyEyeAng); - for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); - - decl Float:flViewPunch[3]; - GetProfileVector(sProfile, "attack_punchvel", flViewPunch); - - decl Float:flTargetDist; - decl Handle:hTrace; - - new Float:flAttackRange = NPCChaserGetAttackRange(iBossIndex, 0); - new Float:flAttackFOV = NPCChaserGetAttackSpread(iBossIndex, 0); - new Float:flAttackDamageForce = NPCChaserGetAttackDamageForce(iBossIndex, 0); - - new bool:bHit = false; - - { - new prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bHit = true; - SDKHooks_TakeDamage(prop, slender, slender, flDamageVsProps, iDamageType, _, _, flMyEyePos); - } - } - - prop = -1; - while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) - { - if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) - { - if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) - { - bHit = true; - SDKHooks_TakeDamage(prop, slender, slender, flDamageVsProps, iDamageType, _, _, flMyEyePos); - } - } - } - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInGhostMode(i)) continue; - - if (!bAttackEliminated && g_bPlayerEliminated[i]) continue; - - decl Float:flTargetPos[3]; - GetClientEyePosition(i, flTargetPos); - - hTrace = TR_TraceRayFilterEx(flMyEyePos, - flTargetPos, - MASK_NPCSOLID, - RayType_EndPoint, - TraceRayDontHitEntity, - slender); - - new bool:bTraceDidHit = TR_DidHit(hTrace); - new iTraceHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (bTraceDidHit && iTraceHitEntity != i) - { - decl Float:flTargetMins[3], Float:flTargetMaxs[3]; - GetEntPropVector(i, Prop_Send, "m_vecMins", flTargetMins); - GetEntPropVector(i, Prop_Send, "m_vecMaxs", flTargetMaxs); - GetClientAbsOrigin(i, flTargetPos); - for (new i2 = 0; i2 < 3; i2++) flTargetPos[i2] += ((flTargetMins[i2] + flTargetMaxs[i2]) / 2.0); - - hTrace = TR_TraceRayFilterEx(flMyEyePos, - flTargetPos, - MASK_NPCSOLID, - RayType_EndPoint, - TraceRayDontHitEntity, - slender); - - bTraceDidHit = TR_DidHit(hTrace); - iTraceHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - } - - if (!bTraceDidHit || iTraceHitEntity == i) - { - flTargetDist = GetVectorDistance(flTargetPos, flMyEyePos); - - if (flTargetDist <= flAttackRange) - { - decl Float:flDirection[3]; - SubtractVectors(flTargetPos, flMyEyePos, flDirection); - GetVectorAngles(flDirection, flDirection); - - if (FloatAbs(AngleDiff(flDirection[1], flMyEyeAng[1])) <= flAttackFOV) - { - bHit = true; - GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flDirection, flDirection); - ScaleVector(flDirection, flAttackDamageForce); - - Call_StartForward(fOnClientDamagedByBoss); - Call_PushCell(i); - Call_PushCell(iBossIndex); - Call_PushCell(slender); - Call_PushFloat(flDamage); - Call_PushCell(iDamageType); - Call_Finish(); - - SDKHooks_TakeDamage(i, slender, slender, flDamage, iDamageType, _, flDirection, flMyEyePos); - ClientViewPunch(i, flViewPunch); - - if (NPCHasAttribute(iBossIndex, "bleed player on hit")) - { - new Float:flDuration = NPCGetAttributeValue(iBossIndex, "bleed player on hit"); - if (flDuration > 0.0) - { - TF2_MakeBleed(i, slender, flDuration); - } - } - - // Add stress - new Float:flStressScalar = flDamage / 125.0; - if (flStressScalar > 1.0) flStressScalar = 1.0; - ClientAddStress(i, 0.33 * flStressScalar); - } - } - } - } - - decl String:sSoundPath[PLATFORM_MAX_PATH]; - - if (bHit) - { - // Fling it. - new phys = CreateEntityByName("env_physexplosion"); - if (phys != -1) - { - TeleportEntity(phys, flMyEyePos, NULL_VECTOR, NULL_VECTOR); - DispatchKeyValue(phys, "spawnflags", "1"); - DispatchKeyValueFloat(phys, "radius", flAttackRange); - DispatchKeyValueFloat(phys, "magnitude", flAttackDamageForce); - DispatchSpawn(phys); - ActivateEntity(phys); - AcceptEntityInput(phys, "Explode"); - AcceptEntityInput(phys, "Kill"); - } - - GetRandomStringFromProfile(sProfile, "sound_hitenemy", sSoundPath, sizeof(sSoundPath)); - if (sSoundPath[0]) EmitSoundToAll(sSoundPath, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - } - else - { - GetRandomStringFromProfile(sProfile, "sound_missenemy", sSoundPath, sizeof(sSoundPath)); - if (sSoundPath[0]) EmitSoundToAll(sSoundPath, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - } - - g_hSlenderAttackTimer[iBossIndex] = CreateTimer(GetProfileFloat(sProfile, "attack_endafter"), Timer_SlenderChaseBossAttackEnd, entref, TIMER_FLAG_NO_MAPCHANGE); -} - -static NPCAttackValidateTarget(iBossIndex, iTarget, Float:flAttackRange, Float:flAttackFOV) -{ - new iBoss = NPCGetEntIndex(iBossIndex); - - decl Float:flMyEyePos[3], Float:flMyEyeAng[3]; - NPCGetEyePosition(iBossIndex, flMyEyePos); - GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); - AddVectors(g_flSlenderEyeAngOffset[iBossIndex], flMyEyeAng, flMyEyeAng); - for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); - - decl Float:flTargetPos[3], Float:flTargetMins[3], Float:flTargetMaxs[3]; - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTargetPos); - GetEntPropVector(iTarget, Prop_Send, "m_vecMins", flTargetMins); - GetEntPropVector(iTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); - - for (new i = 0; i < 3; i++) - { - flTargetPos[i] += (flTargetMins[i] + flTargetMaxs[i]) / 2.0; - } - - new Float:flTargetDist = GetVectorDistance(flTargetPos, flMyEyePos); - if (flTargetDist <= flAttackRange) - { - decl Float:flDirection[3]; - SubtractVectors(g_flSlenderGoalPos[iBossIndex], flMyEyePos, flDirection); - GetVectorAngles(flDirection, flDirection); - - if (FloatAbs(AngleDiff(flDirection[1], flMyEyeAng[1])) <= flAttackFOV / 2.0) - { - new Handle:hTrace = TR_TraceRayFilterEx(flMyEyePos, - flTargetPos, - MASK_NPCSOLID, - RayType_EndPoint, - TraceRayDontHitEntity, - iBoss); - - new bool:bTraceDidHit = TR_DidHit(hTrace); - new iTraceHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (!bTraceDidHit || iTraceHitEntity == iTarget) - { - return true; - } - } - } - - return false; -} - -public Action:Timer_SlenderChaseBossAttackEnd(Handle:timer, any:entref) -{ - if (!g_bEnabled) return; - - new slender = EntRefToEntIndex(entref); - if (!slender || slender == INVALID_ENT_REFERENCE) return; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return; - - if (timer != g_hSlenderAttackTimer[iBossIndex]) return; - - g_bSlenderAttacking[iBossIndex] = false; - g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; +#if defined _sf2_npc_chaser_included + #endinput +#endif +#define _sf2_npc_chaser_included + +static Float:g_flNPCStepSize[MAX_BOSSES]; + +static Float:g_flNPCWalkSpeed[MAX_BOSSES][Difficulty_Max]; +static Float:g_flNPCAirSpeed[MAX_BOSSES][Difficulty_Max]; + +static Float:g_flNPCMaxWalkSpeed[MAX_BOSSES][Difficulty_Max]; +static Float:g_flNPCMaxAirSpeed[MAX_BOSSES][Difficulty_Max]; + +static Float:g_flNPCWakeRadius[MAX_BOSSES]; + +static bool:g_bNPCStunEnabled[MAX_BOSSES]; +static Float:g_flNPCStunDuration[MAX_BOSSES]; +static bool:g_bNPCStunFlashlightEnabled[MAX_BOSSES]; +static Float:g_flNPCStunFlashlightDamage[MAX_BOSSES]; +static Float:g_flNPCStunInitialHealth[MAX_BOSSES]; +static Float:g_flNPCStunHealth[MAX_BOSSES]; + +static g_iNPCState[MAX_BOSSES] = { -1, ... }; +static g_iNPCMovementActivity[MAX_BOSSES] = { -1, ... }; + +enum SF2NPCChaser_BaseAttackStructure +{ + SF2NPCChaser_BaseAttackType, + Float:SF2NPCChaser_BaseAttackDamage, + Float:SF2NPCChaser_BaseAttackDamageVsProps, + Float:SF2NPCChaser_BaseAttackDamageForce, + SF2NPCChaser_BaseAttackDamageType, + Float:SF2NPCChaser_BaseAttackDamageDelay, + Float:SF2NPCChaser_BaseAttackRange, + Float:SF2NPCChaser_BaseAttackDuration, + Float:SF2NPCChaser_BaseAttackSpread, + Float:SF2NPCChaser_BaseAttackBeginRange, + Float:SF2NPCChaser_BaseAttackBeginFOV, + Float:SF2NPCChaser_BaseAttackCooldown, + Float:SF2NPCChaser_BaseAttackNextAttackTime +}; + +static g_NPCBaseAttacks[MAX_BOSSES][SF2_CHASER_BOSS_MAX_ATTACKS][SF2NPCChaser_BaseAttackStructure]; + +#if defined METHODMAPS + +const SF2NPC_Chaser SF2_INVALID_NPC_CHASER = SF2NPC_Chaser:-1; + + +methodmap SF2NPC_Chaser < SF2NPC_BaseNPC +{ + property float WakeRadius + { + public get() { return NPCChaserGetWakeRadius(this.Index); } + } + + property float StepSize + { + public get() { return NPCChaserGetStepSize(this.Index); } + } + + property bool StunEnabled + { + public get() { return NPCChaserIsStunEnabled(this.Index); } + } + + property bool StunByFlashlightEnabled + { + public get() { return NPCChaserIsStunByFlashlightEnabled(this.Index); } + } + + property float StunFlashlightDamage + { + public get() { return NPCChaserGetStunFlashlightDamage(this.Index); } + } + + property float StunDuration + { + public get() { return NPCChaserGetStunDuration(this.Index); } + } + + property float StunHealth + { + public get() { return NPCChaserGetStunHealth(this.Index); } + public set(float amount) { NPCChaserSetStunHealth(this.Index, amount); } + } + + property float StunInitialHealth + { + public get() { return NPCChaserGetStunInitialHealth(this.Index); } + } + + property int State + { + public get() { return NPCChaserGetState(this.Index); } + public set(int state) { NPCChaserSetState(this.Index, state); } + } + + property int MovementActivity + { + public get() { return NPCChaserGetMovementActivity(this.Index); } + public set(int movementActivity) { NPCChaserSetMovementActivity(this.Index, movementActivity); } + } + + public SF2NPC_Chaser(int index) + { + return SF2NPC_Chaser:SF2NPC_BaseNPC(index); + } + + public float GetWalkSpeed(int difficulty) + { + return NPCChaserGetWalkSpeed(this.Index, difficulty); + } + + public void SetWalkSpeed(int difficulty, float amount) + { + NPCChaserSetWalkSpeed(this.Index, difficulty, amount); + } + + public float GetAirSpeed(int difficulty) + { + return NPCChaserGetAirSpeed(this.Index, difficulty); + } + + public void SetAirSpeed(int difficulty, float amount) + { + NPCChaserSetAirSpeed(this.Index, difficulty, amount); + } + + public float GetMaxWalkSpeed(int difficulty) + { + return NPCChaserGetMaxWalkSpeed(this.Index, difficulty); + } + + public void SetMaxWalkSpeed(int difficulty, float amount) + { + NPCChaserSetMaxWalkSpeed(this.Index, difficulty, amount); + } + + public float GetMaxAirSpeed(int difficulty) + { + return NPCChaserGetMaxAirSpeed(this.Index, difficulty); + } + + public void SetMaxAirSpeed(int difficulty, float amount) + { + NPCChaserSetMaxAirSpeed(this.Index, difficulty, amount); + } + + public void AddStunHealth(float amount) + { + NPCChaserAddStunHealth(this.Index, amount); + } +} + +#endif + +public NPCChaserInitialize() +{ + for (new iNPCIndex = 0; iNPCIndex < MAX_BOSSES; iNPCIndex++) + { + NPCChaserResetValues(iNPCIndex); + } +} + +Float:NPCChaserGetWalkSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCWalkSpeed[iNPCIndex][iDifficulty]; +} + +NPCChaserSetWalkSpeed(iNPCIndex, iDifficulty, Float:flAmount) +{ + g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = flAmount; +} + +Float:NPCChaserGetAirSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCAirSpeed[iNPCIndex][iDifficulty]; +} + +NPCChaserSetAirSpeed(iNPCIndex, iDifficulty, Float:flAmount) +{ + g_flNPCAirSpeed[iNPCIndex][iDifficulty] = flAmount; +} + +Float:NPCChaserGetMaxWalkSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty]; +} + +NPCChaserSetMaxWalkSpeed(iNPCIndex, iDifficulty, Float:flAmount) +{ + g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = flAmount; +} + +Float:NPCChaserGetMaxAirSpeed(iNPCIndex, iDifficulty) +{ + return g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty]; +} + +NPCChaserSetMaxAirSpeed(iNPCIndex, iDifficulty, Float:flAmount) +{ + g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = flAmount; +} + +Float:NPCChaserGetWakeRadius(iNPCIndex) +{ + return g_flNPCWakeRadius[iNPCIndex]; +} + +Float:NPCChaserGetStepSize(iNPCIndex) +{ + return g_flNPCStepSize[iNPCIndex]; +} + +NPCChaserGetAttackType(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackType]; +} + +Float:NPCChaserGetAttackDamage(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamage]; +} + +Float:NPCChaserGetAttackDamageVsProps(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageVsProps]; +} + +Float:NPCChaserGetAttackDamageForce(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageForce]; +} + +NPCChaserGetAttackDamageType(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageType]; +} + +Float:NPCChaserGetAttackDamageDelay(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDamageDelay]; +} + +Float:NPCChaserGetAttackRange(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackRange]; +} + +Float:NPCChaserGetAttackDuration(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackDuration]; +} + +Float:NPCChaserGetAttackSpread(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackSpread]; +} + +Float:NPCChaserGetAttackBeginRange(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackBeginRange]; +} + +Float:NPCChaserGetAttackBeginFOV(iNPCIndex, iAttackIndex) +{ + return g_NPCBaseAttacks[iNPCIndex][iAttackIndex][SF2NPCChaser_BaseAttackBeginFOV]; +} + +bool:NPCChaserIsStunEnabled(iNPCIndex) +{ + return g_bNPCStunEnabled[iNPCIndex]; +} + +bool:NPCChaserIsStunByFlashlightEnabled(iNPCIndex) +{ + return g_bNPCStunFlashlightEnabled[iNPCIndex]; +} + +Float:NPCChaserGetStunFlashlightDamage(iNPCIndex) +{ + return g_flNPCStunFlashlightDamage[iNPCIndex]; +} + +Float:NPCChaserGetStunDuration(iNPCIndex) +{ + return g_flNPCStunDuration[iNPCIndex]; +} + +Float:NPCChaserGetStunHealth(iNPCIndex) +{ + return g_flNPCStunHealth[iNPCIndex]; +} + +NPCChaserSetStunHealth(iNPCIndex, Float:flAmount) +{ + g_flNPCStunHealth[iNPCIndex] = flAmount; +} + +NPCChaserAddStunHealth(iNPCIndex, Float:flAmount) +{ + NPCChaserSetStunHealth(iNPCIndex, NPCChaserGetStunHealth(iNPCIndex) + flAmount); +} + +Float:NPCChaserGetStunInitialHealth(iNPCIndex) +{ + return g_flNPCStunInitialHealth[iNPCIndex]; +} + +NPCChaserGetState(iNPCIndex) +{ + return g_iNPCState[iNPCIndex]; +} + +NPCChaserSetState(iNPCIndex, iState) +{ + g_iNPCState[iNPCIndex] = iState; +} + +NPCChaserGetMovementActivity(iNPCIndex) +{ + return g_iNPCMovementActivity[iNPCIndex]; +} + +NPCChaserSetMovementActivity(iNPCIndex, iMovementActivity) +{ + g_iNPCMovementActivity[iNPCIndex] = iMovementActivity; +} + +NPCChaserOnSelectProfile(iNPCIndex) +{ + new iUniqueProfileIndex = NPCGetUniqueProfileIndex(iNPCIndex); + + g_flNPCWakeRadius[iNPCIndex] = GetChaserProfileWakeRadius(iUniqueProfileIndex); + g_flNPCStepSize[iNPCIndex] = GetChaserProfileStepSize(iUniqueProfileIndex); + + for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) + { + g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = GetChaserProfileWalkSpeed(iUniqueProfileIndex, iDifficulty); + g_flNPCAirSpeed[iNPCIndex][iDifficulty] = GetChaserProfileAirSpeed(iUniqueProfileIndex, iDifficulty); + + g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = GetChaserProfileMaxWalkSpeed(iUniqueProfileIndex, iDifficulty); + g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = GetChaserProfileMaxAirSpeed(iUniqueProfileIndex, iDifficulty); + } + + // Get attack data. + for (new i = 0; i < GetChaserProfileAttackCount(iUniqueProfileIndex); i++) + { + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackType] = GetChaserProfileAttackType(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamage] = GetChaserProfileAttackDamage(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageVsProps] = GetChaserProfileAttackDamageVsProps(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageForce] = GetChaserProfileAttackDamageForce(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageType] = GetChaserProfileAttackDamageType(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageDelay] = GetChaserProfileAttackDamageDelay(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackRange] = GetChaserProfileAttackRange(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDuration] = GetChaserProfileAttackDuration(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackSpread] = GetChaserProfileAttackSpread(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginRange] = GetChaserProfileAttackBeginRange(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginFOV] = GetChaserProfileAttackBeginFOV(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackCooldown] = GetChaserProfileAttackCooldown(iUniqueProfileIndex, i); + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackNextAttackTime] = -1.0; + } + + // Get stun data. + g_bNPCStunEnabled[iNPCIndex] = GetChaserProfileStunState(iUniqueProfileIndex); + g_flNPCStunDuration[iNPCIndex] = GetChaserProfileStunDuration(iUniqueProfileIndex); + g_bNPCStunFlashlightEnabled[iNPCIndex] = GetChaserProfileStunFlashlightState(iUniqueProfileIndex); + g_flNPCStunFlashlightDamage[iNPCIndex] = GetChaserProfileStunFlashlightDamage(iUniqueProfileIndex); + g_flNPCStunInitialHealth[iNPCIndex] = GetChaserProfileStunHealth(iUniqueProfileIndex); + + NPCChaserSetStunHealth(iNPCIndex, NPCChaserGetStunInitialHealth(iNPCIndex)); +} + +NPCChaserOnRemoveProfile(iNPCIndex) +{ + NPCChaserResetValues(iNPCIndex); +} + +/** + * Resets all global variables on a specified NPC. Usually this should be done last upon removing a boss from the game. + */ +static NPCChaserResetValues(iNPCIndex) +{ + g_flNPCWakeRadius[iNPCIndex] = 0.0; + g_flNPCStepSize[iNPCIndex] = 0.0; + + for (new iDifficulty = 0; iDifficulty < Difficulty_Max; iDifficulty++) + { + g_flNPCWalkSpeed[iNPCIndex][iDifficulty] = 0.0; + g_flNPCAirSpeed[iNPCIndex][iDifficulty] = 0.0; + + g_flNPCMaxWalkSpeed[iNPCIndex][iDifficulty] = 0.0; + g_flNPCMaxAirSpeed[iNPCIndex][iDifficulty] = 0.0; + } + + // Clear attack data. + for (new i = 0; i < SF2_CHASER_BOSS_MAX_ATTACKS; i++) + { + // Base attack data. + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackType] = SF2BossAttackType_Invalid; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamage] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageVsProps] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageForce] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageType] = 0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDamageDelay] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackRange] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackDuration] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackSpread] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginRange] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackBeginFOV] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackCooldown] = 0.0; + g_NPCBaseAttacks[iNPCIndex][i][SF2NPCChaser_BaseAttackNextAttackTime] = -1.0; + } + + g_bNPCStunEnabled[iNPCIndex] = false; + g_flNPCStunDuration[iNPCIndex] = 0.0; + g_bNPCStunFlashlightEnabled[iNPCIndex] = false; + g_flNPCStunInitialHealth[iNPCIndex] = 0.0; + + NPCChaserSetStunHealth(iNPCIndex, 0.0); + + g_iNPCState[iNPCIndex] = -1; + g_iNPCMovementActivity[iNPCIndex] = -1; +} + +// So this is how the thought process of the bosses should go. +// 1. Search for enemy; either by sight or by sound. +// - Any noticeable sounds should be investigated. +// - Too many sounds will put me in alert mode. +// 2. Alert of an enemy; I saw something or I heard something unusual +// - Go to the position where I last heard the sound. +// - Keep on searching until I give up. Then drop back to idle mode. +// 3. Found an enemy! Give chase! +// - Keep on chasing until enemy is killed or I give up. +// - Keep a path in memory as long as I still have him in my sights. +// - If I lose sight or I'm unable to traverse safely, find paths around obstacles and follow memorized path. +// - If I reach the end of my path and I still don't see him and I still want to pursue him, keep on going in the direction I'm going. + +stock bool:IsTargetValidForSlender(iTarget, bool:bIncludeEliminated=false) +{ + if (!iTarget || !IsValidEntity(iTarget)) return false; + + if (IsValidClient(iTarget)) + { + if (!IsClientInGame(iTarget) || + !IsPlayerAlive(iTarget) || + IsClientInDeathCam(iTarget) || + (!bIncludeEliminated && g_bPlayerEliminated[iTarget]) || + IsClientInGhostMode(iTarget) || + DidClientEscape(iTarget)) return false; + } + + return true; +} + +public Action:Timer_SlenderChaseBossThink(Handle:timer, any:entref) +{ + if (!g_bEnabled) return Plugin_Stop; + + new slender = EntRefToEntIndex(entref); + if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return Plugin_Stop; + + if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; + + if (NPCGetFlags(iBossIndex) & SFF_MARKEDASFAKE) return Plugin_Stop; + + decl Float:flSlenderVelocity[3], Float:flMyPos[3], Float:flMyEyeAng[3]; + new Float:flBuffer[3]; + + decl String:sSlenderProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sSlenderProfile, sizeof(sSlenderProfile)); + + GetEntPropVector(slender, Prop_Data, "m_vecAbsVelocity", flSlenderVelocity); + GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flMyPos); + GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", flMyEyeAng); + AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[iBossIndex], flMyEyeAng); + for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); + + new iDifficulty = GetConVarInt(g_cvDifficulty); + + new Float:flVelocityRatio; + new Float:flVelocityRatioWalk; + + new Float:flOriginalSpeed = NPCGetSpeed(iBossIndex, iDifficulty); + new Float:flOriginalWalkSpeed = NPCChaserGetWalkSpeed(iBossIndex, iDifficulty); + new Float:flMaxSpeed = NPCGetMaxSpeed(iBossIndex, iDifficulty); + new Float:flMaxWalkSpeed = NPCChaserGetMaxWalkSpeed(iBossIndex, iDifficulty); + + new Float:flSpeed = flOriginalSpeed * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier; + if (flSpeed < flOriginalSpeed) flSpeed = flOriginalSpeed; + if (flSpeed > flMaxSpeed) flSpeed = flMaxSpeed; + + new Float:flWalkSpeed = flOriginalWalkSpeed * NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier; + if (flWalkSpeed < flOriginalWalkSpeed) flWalkSpeed = flOriginalWalkSpeed; + if (flWalkSpeed > flMaxWalkSpeed) flWalkSpeed = flMaxWalkSpeed; + + if (PeopleCanSeeSlender(iBossIndex, _, false)) + { + if (NPCHasAttribute(iBossIndex, "reduced speed on look")) + { + flSpeed *= NPCGetAttributeValue(iBossIndex, "reduced speed on look"); + } + + if (NPCHasAttribute(iBossIndex, "reduced walk speed on look")) + { + flWalkSpeed *= NPCGetAttributeValue(iBossIndex, "reduced walk speed on look"); + } + } + + g_flSlenderCalculatedWalkSpeed[iBossIndex] = flWalkSpeed; + g_flSlenderCalculatedSpeed[iBossIndex] = flSpeed; + + if (flOriginalSpeed <= 0.0) flVelocityRatio = 0.0; + else flVelocityRatio = GetVectorLength(flSlenderVelocity) / flOriginalSpeed; + + if (flOriginalWalkSpeed <= 0.0) flVelocityRatioWalk = 0.0; + else flVelocityRatioWalk = GetVectorLength(flSlenderVelocity) / flOriginalWalkSpeed; + + new Float:flAttackRange = NPCChaserGetAttackRange(iBossIndex, 0); + new Float:flAttackFOV = NPCChaserGetAttackSpread(iBossIndex, 0); + new Float:flAttackBeginRange = NPCChaserGetAttackBeginRange(iBossIndex, 0); + new Float:flAttackBeginFOV = NPCChaserGetAttackBeginFOV(iBossIndex, 0); + + + new iOldState = g_iSlenderState[iBossIndex]; + new iOldTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); + + new iBestNewTarget = INVALID_ENT_REFERENCE; + new Float:flSearchRange = NPCGetSearchRadius(iBossIndex); + new Float:flBestNewTargetDist = flSearchRange; + new iState = iOldState; + + new bool:bPlayerInFOV[MAXPLAYERS + 1]; + new bool:bPlayerNear[MAXPLAYERS + 1]; + new Float:flPlayerDists[MAXPLAYERS + 1]; + new bool:bPlayerVisible[MAXPLAYERS + 1]; + + new bool:bAttackEliminated = bool:(NPCGetFlags(iBossIndex) & SFF_ATTACKWAITERS); + new bool:bStunEnabled = NPCChaserIsStunEnabled(iBossIndex); + + decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; + GetEntPropVector(slender, Prop_Send, "m_vecMins", flSlenderMins); + GetEntPropVector(slender, Prop_Send, "m_vecMaxs", flSlenderMaxs); + + decl Float:flTraceMins[3], Float:flTraceMaxs[3]; + flTraceMins[0] = flSlenderMins[0]; + flTraceMins[1] = flSlenderMins[1]; + flTraceMins[2] = 0.0; + flTraceMaxs[0] = flSlenderMaxs[0]; + flTraceMaxs[1] = flSlenderMaxs[1]; + flTraceMaxs[2] = 0.0; + + // Gather data about the players around me and get the best new target, in case my old target is invalidated. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsTargetValidForSlender(i, bAttackEliminated)) continue; + + decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; + NPCGetEyePosition(iBossIndex, flTraceStartPos); + GetClientEyePosition(i, flTraceEndPos); + + new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flTraceEndPos, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayBossVisibility, + slender); + + new bool:bIsVisible = !TR_DidHit(hTrace); + new iTraceHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (!bIsVisible && iTraceHitEntity == i) bIsVisible = true; + + bPlayerVisible[i] = bIsVisible; + + // Near radius check. + if (bIsVisible && + GetVectorDistance(flTraceStartPos, flTraceEndPos) <= NPCChaserGetWakeRadius(iBossIndex)) + { + bPlayerNear[i] = true; + } + + // FOV check. + SubtractVectors(flTraceEndPos, flTraceStartPos, flBuffer); + GetVectorAngles(flBuffer, flBuffer); + + if (FloatAbs(AngleDiff(flMyEyeAng[1], flBuffer[1])) <= (NPCGetFOV(iBossIndex) * 0.5)) + { + bPlayerInFOV[i] = true; + } + + new Float:flDist; + new Float:flPriorityValue = g_iPageMax > 0 ? (float(g_iPlayerPageCount[i]) / float(g_iPageMax)) : 0.0; + + if (TF2_GetPlayerClass(i) == TFClass_Medic) flPriorityValue += 0.72; + + flDist = GetVectorDistance(flTraceStartPos, flTraceEndPos); + flPlayerDists[i] = flDist; + + if ((bPlayerNear[i] && iState != STATE_CHASE && iState != STATE_ALERT) || (bIsVisible && bPlayerInFOV[i])) + { + decl Float:flTargetPos[3]; + GetClientAbsOrigin(i, flTargetPos); + + if (flDist <= flSearchRange) + { + // Subtract distance to increase priority. + flDist -= (flDist * flPriorityValue); + + if (flDist < flBestNewTargetDist) + { + iBestNewTarget = i; + flBestNewTargetDist = flDist; + g_iSlenderInterruptConditions[iBossIndex] |= COND_SAWENEMY; + } + + g_flSlenderLastFoundPlayer[iBossIndex][i] = GetGameTime(); + g_flSlenderLastFoundPlayerPos[iBossIndex][i][0] = flTargetPos[0]; + g_flSlenderLastFoundPlayerPos[iBossIndex][i][1] = flTargetPos[1]; + g_flSlenderLastFoundPlayerPos[iBossIndex][i][2] = flTargetPos[2]; + } + } + } + + new bool:bInFlashlight = false; + + // Check to see if someone is facing at us with flashlight on. Only if I'm facing them too. BLINDNESS! + for (new i = 1; i <= MaxClients; i++) + { + if (!IsTargetValidForSlender(i, bAttackEliminated)) continue; + + if (!IsClientUsingFlashlight(i) || !bPlayerInFOV[i]) continue; + + decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; + GetClientEyePosition(i, flTraceStartPos); + NPCGetEyePosition(iBossIndex, flTraceEndPos); + + if (GetVectorDistance(flTraceStartPos, flTraceEndPos) <= SF2_FLASHLIGHT_LENGTH) + { + decl Float:flEyeAng[3], Float:flRequiredAng[3]; + GetClientEyeAngles(i, flEyeAng); + SubtractVectors(flTraceEndPos, flTraceStartPos, flRequiredAng); + GetVectorAngles(flRequiredAng, flRequiredAng); + + if ((FloatAbs(AngleDiff(flEyeAng[0], flRequiredAng[0])) + FloatAbs(AngleDiff(flEyeAng[1], flRequiredAng[1]))) <= 45.0) + { + new Handle:hTrace = TR_TraceRayFilterEx(flTraceStartPos, + flTraceEndPos, + MASK_PLAYERSOLID, + RayType_EndPoint, + TraceRayBossVisibility, + slender); + + new bool:bDidHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (!bDidHit) + { + bInFlashlight = true; + break; + } + } + } + } + + // Damage us if we're in a flashlight. + if (bInFlashlight) + { + if (bStunEnabled) + { + if (NPCChaserIsStunByFlashlightEnabled(iBossIndex)) + { + if (NPCChaserGetStunHealth(iBossIndex) > 0) + { + NPCChaserAddStunHealth(iBossIndex, -NPCChaserGetStunFlashlightDamage(iBossIndex)); + } + } + } + } + + // Process the target that we should have. + new iTarget = iOldTarget; + + /* + if (IsValidEdict(iBestNewTarget)) + { + iTarget = iBestNewTarget; + g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); + } + */ + + if (iTarget && iTarget != INVALID_ENT_REFERENCE) + { + if (!IsTargetValidForSlender(iTarget, bAttackEliminated)) + { + // Clear our target; he's not valid anymore. + iOldTarget = iTarget; + iTarget = INVALID_ENT_REFERENCE; + g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; + } + } + else + { + // Clear our target; he's not valid anymore. + iOldTarget = iTarget; + iTarget = INVALID_ENT_REFERENCE; + g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; + } + + new iInterruptConditions = g_iSlenderInterruptConditions[iBossIndex]; + new bool:bQueueForNewPath = false; + + // Process which state we should be in. + switch (iState) + { + case STATE_IDLE, STATE_WANDER: + { + if (iState == STATE_WANDER) + { + if (GetArraySize(g_hSlenderPath[iBossIndex]) <= 0) + { + iState = STATE_IDLE; + } + } + else + { + if (GetGameTime() >= g_flSlenderNextWanderPos[iBossIndex] && GetRandomFloat(0.0, 1.0) <= 0.25) + { + iState = STATE_WANDER; + } + } + + if (iInterruptConditions & COND_SAWENEMY) + { + // I saw someone over here. Automatically put me into alert mode. + iState = STATE_ALERT; + } + else if (iInterruptConditions & COND_HEARDSUSPICIOUSSOUND) + { + // Sound counts: + // +1 will be added if it hears a footstep. + // +2 will be added if the footstep is someone sprinting. + // +5 will be added if the sound is from a player's weapon hitting an object. + // +10 will be added if a voice command is heard. + // + // Sound counts will be reset after the boss hears a sound after a certain amount of time. + // The purpose of sound counts is to induce boss focusing on sounds suspicious entities are making. + + new iCount = 0; + if (iInterruptConditions & COND_HEARDFOOTSTEP) iCount += 1; + if (iInterruptConditions & COND_HEARDFOOTSTEPLOUD) iCount += 2; + if (iInterruptConditions & COND_HEARDWEAPON) iCount += 5; + if (iInterruptConditions & COND_HEARDVOICE) iCount += 10; + + new bool:bDiscardMasterPos = bool:(GetGameTime() >= g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex]); + + if (GetVectorDistance(g_flSlenderTargetSoundTempPos[iBossIndex], g_flSlenderTargetSoundMasterPos[iBossIndex]) <= GetProfileFloat(sSlenderProfile, "search_sound_pos_dist_tolerance", 512.0) || + bDiscardMasterPos) + { + if (bDiscardMasterPos) g_iSlenderTargetSoundCount[iBossIndex] = 0; + + g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_sound_pos_discard_time", 2.0); + g_flSlenderTargetSoundMasterPos[iBossIndex][0] = g_flSlenderTargetSoundTempPos[iBossIndex][0]; + g_flSlenderTargetSoundMasterPos[iBossIndex][1] = g_flSlenderTargetSoundTempPos[iBossIndex][1]; + g_flSlenderTargetSoundMasterPos[iBossIndex][2] = g_flSlenderTargetSoundTempPos[iBossIndex][2]; + g_iSlenderTargetSoundCount[iBossIndex] += iCount; + } + + if (g_iSlenderTargetSoundCount[iBossIndex] >= GetProfileNum(sSlenderProfile, "search_sound_count_until_alert", 4)) + { + // Someone's making some noise over there! Time to investigate. + g_bSlenderInvestigatingSound[iBossIndex] = true; // This is just so that our sound position would be the goal position. + iState = STATE_ALERT; + } + } + } + case STATE_ALERT: + { + if (GetArraySize(g_hSlenderPath[iBossIndex]) <= 0) + { + // Fully navigated through our path. + iState = STATE_IDLE; + } + else if (GetGameTime() >= g_flSlenderTimeUntilIdle[iBossIndex]) + { + iState = STATE_IDLE; + } + else if (IsValidClient(iBestNewTarget)) + { + if (GetGameTime() >= g_flSlenderTimeUntilChase[iBossIndex] || bPlayerNear[iBestNewTarget]) + { + decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; + NPCGetEyePosition(iBossIndex, flTraceStartPos); + + if (IsValidClient(iBestNewTarget)) GetClientEyePosition(iBestNewTarget, flTraceEndPos); + else + { + decl Float:flTargetMins[3], Float:flTargetMaxs[3]; + GetEntPropVector(iBestNewTarget, Prop_Send, "m_vecMins", flTargetMins); + GetEntPropVector(iBestNewTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); + GetEntPropVector(iBestNewTarget, Prop_Data, "m_vecAbsOrigin", flTraceEndPos); + for (new i = 0; i < 3; i++) flTraceEndPos[i] += ((flTargetMins[i] + flTargetMaxs[i]) / 2.0); + } + + new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flTraceEndPos, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayBossVisibility, + slender); + + new bool:bIsVisible = !TR_DidHit(hTrace); + new iTraceHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (!bIsVisible && iTraceHitEntity == iBestNewTarget) bIsVisible = true; + + if ((bPlayerNear[iBestNewTarget] || bPlayerInFOV[iBestNewTarget]) && bPlayerVisible[iBestNewTarget]) + { + // AHAHAHAH! I GOT YOU NOW! + iTarget = iBestNewTarget; + g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); + iState = STATE_CHASE; + } + } + } + else + { + if (iInterruptConditions & COND_SAWENEMY) + { + if (IsValidClient(iBestNewTarget)) + { + g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][0]; + g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][1]; + g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderLastFoundPlayerPos[iBossIndex][iBestNewTarget][2]; + + bQueueForNewPath = true; + } + } + else if (iInterruptConditions & COND_HEARDSUSPICIOUSSOUND) + { + new bool:bDiscardMasterPos = bool:(GetGameTime() >= g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex]); + + if (GetVectorDistance(g_flSlenderTargetSoundTempPos[iBossIndex], g_flSlenderTargetSoundMasterPos[iBossIndex]) <= GetProfileFloat(sSlenderProfile, "search_sound_pos_dist_tolerance", 512.0) || + bDiscardMasterPos) + { + g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_sound_pos_discard_time", 2.0); + g_flSlenderTargetSoundMasterPos[iBossIndex][0] = g_flSlenderTargetSoundTempPos[iBossIndex][0]; + g_flSlenderTargetSoundMasterPos[iBossIndex][1] = g_flSlenderTargetSoundTempPos[iBossIndex][1]; + g_flSlenderTargetSoundMasterPos[iBossIndex][2] = g_flSlenderTargetSoundTempPos[iBossIndex][2]; + + // We have to manually set the goal position here because the goal position will not be changed due to no change in state. + g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderTargetSoundMasterPos[iBossIndex][0]; + g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderTargetSoundMasterPos[iBossIndex][1]; + g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderTargetSoundMasterPos[iBossIndex][2]; + + g_bSlenderInvestigatingSound[iBossIndex] = true; + + bQueueForNewPath = true; + } + } + + new bool:bBlockingProp = false; + + if (NPCGetFlags(iBossIndex) & SFF_ATTACKPROPS) + { + new prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bBlockingProp = true; + break; + } + } + + if (!bBlockingProp) + { + prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) + { + if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bBlockingProp = true; + break; + } + } + } + } + } + + if (bBlockingProp) + { + iState = STATE_ATTACK; + } + } + } + case STATE_CHASE, STATE_ATTACK, STATE_STUN: + { + if (iState == STATE_CHASE) + { + if (IsValidEdict(iTarget)) + { + decl Float:flTraceStartPos[3], Float:flTraceEndPos[3]; + NPCGetEyePosition(iBossIndex, flTraceStartPos); + + if (IsValidClient(iTarget)) + { + GetClientEyePosition(iTarget, flTraceEndPos); + } + else + { + decl Float:flTargetMins[3], Float:flTargetMaxs[3]; + GetEntPropVector(iTarget, Prop_Send, "m_vecMins", flTargetMins); + GetEntPropVector(iTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTraceEndPos); + for (new i = 0; i < 3; i++) flTraceEndPos[i] += ((flTargetMins[i] + flTargetMaxs[i]) / 2.0); + } + + new bool:bIsDeathPosVisible = false; + + if (g_bSlenderChaseDeathPosition[iBossIndex]) + { + new Handle:hTrace = TR_TraceRayFilterEx(flTraceStartPos, + g_flSlenderChaseDeathPosition[iBossIndex], + MASK_NPCSOLID, + RayType_EndPoint, + TraceRayBossVisibility, + slender); + bIsDeathPosVisible = !TR_DidHit(hTrace); + CloseHandle(hTrace); + } + + if (!bPlayerVisible[iTarget]) + { + if (GetArraySize(g_hSlenderPath[iBossIndex]) == 0) + { + iState = STATE_IDLE; + } + else if (GetGameTime() >= g_flSlenderTimeUntilAlert[iBossIndex]) + { + iState = STATE_ALERT; + } + else if (bIsDeathPosVisible) + { + iState = STATE_IDLE; + } + else if (iInterruptConditions & COND_CHASETARGETINVALIDATED) + { + if (!g_bSlenderChaseDeathPosition[iBossIndex]) + { + g_bSlenderChaseDeathPosition[iBossIndex] = true; + } + } + } + else + { + g_bSlenderChaseDeathPosition[iBossIndex] = false; // We're not chasing a dead player after all! Reset. + + decl Float:flAttackDirection[3]; + GetClientAbsOrigin(iTarget, g_flSlenderGoalPos[iBossIndex]); + SubtractVectors(g_flSlenderGoalPos[iBossIndex], flMyPos, flAttackDirection); + GetVectorAngles(flAttackDirection, flAttackDirection); + + if (GetVectorDistance(g_flSlenderGoalPos[iBossIndex], flMyPos) <= flAttackBeginRange && + (FloatAbs(AngleDiff(flAttackDirection[0], flMyEyeAng[0])) + FloatAbs(AngleDiff(flAttackDirection[1], flMyEyeAng[1]))) <= flAttackBeginFOV / 2.0) + { + // ENOUGH TALK! HAVE AT YOU! + iState = STATE_ATTACK; + } + else + { + new bool:bBlockingProp = false; + + if (NPCGetFlags(iBossIndex) & SFF_ATTACKPROPS) + { + new prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bBlockingProp = true; + break; + } + } + + if (!bBlockingProp) + { + prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) + { + if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bBlockingProp = true; + break; + } + } + } + } + } + + if (bBlockingProp) + { + iState = STATE_ATTACK; + } + else if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) + { + g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; + bQueueForNewPath = true; + } + } + } + } + else + { + // Even if the target isn't valid anymore, see if I still have some ways to go on my current path, + // because I shouldn't actually know that the target has died until I see it. + if (GetArraySize(g_hSlenderPath[iBossIndex]) == 0) + { + iState = STATE_IDLE; + } + } + } + else if (iState == STATE_ATTACK) + { + if (!g_bSlenderAttacking[iBossIndex]) + { + if (IsValidClient(iTarget)) + { + g_bSlenderChaseDeathPosition[iBossIndex] = false; + + // Chase him again! + iState = STATE_CHASE; + } + else + { + // Target isn't valid anymore. We killed him, Mac! + iState = STATE_ALERT; + } + } + } + else if (iState == STATE_STUN) + { + if (GetGameTime() >= g_flSlenderTimeUntilRecover[iBossIndex]) + { + NPCChaserSetStunHealth(iBossIndex, NPCChaserGetStunInitialHealth(iBossIndex)); + + if (IsValidClient(iTarget)) + { + // Chase him again! + iState = STATE_CHASE; + } + else + { + // WHAT DA FUUUUUUUUUUUQ. TARGET ISN'T VALID. AUSDHASUIHD + iState = STATE_ALERT; + } + } + } + } + } + + new bool:bDoChasePersistencyInit = false; + + if (iState != STATE_STUN) + { + if (bStunEnabled) + { + if (NPCChaserGetStunHealth(iBossIndex) <= 0) + { + if (iState != STATE_CHASE && iState != STATE_ATTACK) + { + // Sometimes players can stun the boss while it's not in chase mode. If that happens, we + // need to set the persistency value to the chase initial value. + bDoChasePersistencyInit = true; + } + + iState = STATE_STUN; + } + } + } + + // Finally, set our new state. + g_iSlenderState[iBossIndex] = iState; + + decl String:sAnimation[64]; + new iModel = EntRefToEntIndex(g_iSlenderModel[iBossIndex]); + + new Float:flPlaybackRateWalk = g_flSlenderWalkAnimationPlaybackRate[iBossIndex]; + new Float:flPlaybackRateRun = g_flSlenderRunAnimationPlaybackRate[iBossIndex]; + new Float:flPlaybackRateIdle = g_flSlenderIdleAnimationPlaybackRate[iBossIndex]; + + if (iOldState != iState) + { + switch (iState) + { + case STATE_IDLE, STATE_WANDER: + { + g_iSlenderTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; + g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; + g_flSlenderTimeUntilChase[iBossIndex] = -1.0; + g_bSlenderChaseDeathPosition[iBossIndex] = false; + + if (iOldState != STATE_IDLE && iOldState != STATE_WANDER) + { + g_iSlenderTargetSoundCount[iBossIndex] = 0; + g_bSlenderInvestigatingSound[iBossIndex] = false; + g_flSlenderTargetSoundDiscardMasterPosTime[iBossIndex] = -1.0; + + g_flSlenderTimeUntilKill[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "idle_lifetime", 10.0); + } + + if (iState == STATE_WANDER) + { + // Force new wander position. + g_flSlenderNextWanderPos[iBossIndex] = -1.0; + } + + // Animation handling. + if (iModel && iModel != INVALID_ENT_REFERENCE) + { + if (iState == STATE_WANDER && (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE)) + { + if (GetProfileString(sSlenderProfile, "animation_walk", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateWalk); + } + } + else + { + if (GetProfileString(sSlenderProfile, "animation_idle", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, flPlaybackRateIdle); + } + } + } + } + + case STATE_ALERT: + { + g_bSlenderChaseDeathPosition[iBossIndex] = false; + + // Set our goal position. + if (g_bSlenderInvestigatingSound[iBossIndex]) + { + g_flSlenderGoalPos[iBossIndex][0] = g_flSlenderTargetSoundMasterPos[iBossIndex][0]; + g_flSlenderGoalPos[iBossIndex][1] = g_flSlenderTargetSoundMasterPos[iBossIndex][1]; + g_flSlenderGoalPos[iBossIndex][2] = g_flSlenderTargetSoundMasterPos[iBossIndex][2]; + } + + g_flSlenderTimeUntilIdle[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_alert_duration", 5.0); + g_flSlenderTimeUntilAlert[iBossIndex] = -1.0; + g_flSlenderTimeUntilChase[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_alert_gracetime", 0.5); + + bQueueForNewPath = true; + + // Animation handling. + if (iModel && iModel != INVALID_ENT_REFERENCE) + { + if (GetProfileString(sSlenderProfile, "animation_walk", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateWalk); + } + } + } + case STATE_CHASE, STATE_ATTACK, STATE_STUN: + { + g_bSlenderInvestigatingSound[iBossIndex] = false; + g_iSlenderTargetSoundCount[iBossIndex] = 0; + + if (iOldState != STATE_ATTACK && iOldState != STATE_CHASE && iOldState != STATE_STUN) + { + g_flSlenderTimeUntilIdle[iBossIndex] = -1.0; + g_flSlenderTimeUntilAlert[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration", 10.0); + g_flSlenderTimeUntilChase[iBossIndex] = -1.0; + + new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init", 5.0); + if (flPersistencyTime >= 0.0) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; + } + } + + if (iState == STATE_ATTACK) + { + g_bSlenderAttacking[iBossIndex] = true; + g_hSlenderAttackTimer[iBossIndex] = CreateTimer(NPCChaserGetAttackDamageDelay(iBossIndex, 0), Timer_SlenderChaseBossAttack, EntIndexToEntRef(slender), TIMER_FLAG_NO_MAPCHANGE); + + new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_attack", -1.0); + if (flPersistencyTime >= 0.0) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; + } + + flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_attack", 2.0); + if (flPersistencyTime >= 0.0) + { + if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); + g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; + } + + SlenderPerformVoice(iBossIndex, "sound_attackenemy"); + } + else if (iState == STATE_STUN) + { + if (g_bSlenderAttacking[iBossIndex]) + { + // Cancel attacking. + g_bSlenderAttacking[iBossIndex] = false; + g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; + } + + if (!bDoChasePersistencyInit) + { + new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_stun", -1.0); + if (flPersistencyTime >= 0.0) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; + } + + flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_stun", 2.0); + if (flPersistencyTime >= 0.0) + { + if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); + g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; + } + } + else + { + new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init", 5.0); + if (flPersistencyTime >= 0.0) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; + } + } + + g_flSlenderTimeUntilRecover[iBossIndex] = GetGameTime() + NPCChaserGetStunDuration(iBossIndex); + + // Sound handling. Ignore time check. + SlenderPerformVoice(iBossIndex, "sound_stun"); + } + else + { + if (iOldState != STATE_ATTACK) + { + // Sound handling. + SlenderPerformVoice(iBossIndex, "sound_chaseenemyinitial"); + } + } + + // Animation handling. + if (iModel && iModel != INVALID_ENT_REFERENCE) + { + if (iState == STATE_CHASE) + { + if (GetProfileString(sSlenderProfile, "animation_run", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, flVelocityRatio * flPlaybackRateRun); + } + } + else if (iState == STATE_ATTACK) + { + if (GetProfileString(sSlenderProfile, "animation_attack", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, GetProfileFloat(sSlenderProfile, "animation_attack_playbackrate", 1.0)); + } + } + else if (iState == STATE_STUN) + { + if (GetProfileString(sSlenderProfile, "animation_stun", sAnimation, sizeof(sAnimation))) + { + EntitySetAnimation(iModel, sAnimation, _, GetProfileFloat(sSlenderProfile, "animation_stun_playbackrate", 1.0)); + } + } + } + } + } + + // Call our forward. + Call_StartForward(fOnBossChangeState); + Call_PushCell(iBossIndex); + Call_PushCell(iOldState); + Call_PushCell(iState); + Call_Finish(); + } + + switch (iState) + { + case STATE_IDLE: + { + // Animation playback speed handling. + if (iModel && iModel != INVALID_ENT_REFERENCE) + { + SetVariantFloat(flPlaybackRateIdle); + AcceptEntityInput(iModel, "SetPlaybackRate"); + } + } + case STATE_WANDER, STATE_ALERT, STATE_CHASE, STATE_ATTACK: + { + // These deal with movement, therefore we need to set our + // destination first. That is, if we don't have one. (nav mesh only) + + if (iState == STATE_WANDER) + { + if (GetGameTime() >= g_flSlenderNextWanderPos[iBossIndex]) + { + new Float:flMin = GetProfileFloat(sSlenderProfile, "search_wander_time_min", 4.0); + new Float:flMax = GetProfileFloat(sSlenderProfile, "search_wander_time_max", 6.5); + g_flSlenderNextWanderPos[iBossIndex] = GetGameTime() + GetRandomFloat(flMin, flMax); + + if (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE) + { + // We're allowed to move in wander mode. Get a new wandering position and create a path to follow. + // If the position can't be reached, then just get to the closest area that we can get. + new Float:flWanderRangeMin = GetProfileFloat(sSlenderProfile, "search_wander_range_min", 400.0); + new Float:flWanderRangeMax = GetProfileFloat(sSlenderProfile, "search_wander_range_max", 1024.0); + new Float:flWanderRange = GetRandomFloat(flWanderRangeMin, flWanderRangeMax); + + decl Float:flWanderPos[3]; + flWanderPos[0] = 0.0; + flWanderPos[1] = GetRandomFloat(0.0, 360.0); + flWanderPos[2] = 0.0; + + GetAngleVectors(flWanderPos, flWanderPos, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flWanderPos, flWanderPos); + ScaleVector(flWanderPos, flWanderRange); + AddVectors(flWanderPos, flMyPos, flWanderPos); + + g_flSlenderGoalPos[iBossIndex][0] = flWanderPos[0]; + g_flSlenderGoalPos[iBossIndex][1] = flWanderPos[1]; + g_flSlenderGoalPos[iBossIndex][2] = flWanderPos[2]; + + bQueueForNewPath = true; + g_flSlenderNextPathTime[iBossIndex] = -1.0; // We're not going to wander around too much, so no need for a time constraint. + } + } + } + else if (iState == STATE_ALERT) + { + if (iInterruptConditions & COND_SAWENEMY) + { + if (IsValidEntity(iBestNewTarget)) + { + if ((bPlayerInFOV[iBestNewTarget] || bPlayerNear[iBestNewTarget]) && bPlayerVisible[iBestNewTarget]) + { + // Constantly update my path if I see him. + if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) + { + GetEntPropVector(iBestNewTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); + bQueueForNewPath = true; + g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; + } + } + } + } + } + else if (iState == STATE_CHASE || iState == STATE_ATTACK) + { + if (IsValidEntity(iBestNewTarget)) + { + iOldTarget = iTarget; + iTarget = iBestNewTarget; + g_iSlenderTarget[iBossIndex] = EntIndexToEntRef(iBestNewTarget); + } + + if (iTarget != INVALID_ENT_REFERENCE) + { + if (iOldTarget != iTarget) + { + // Brand new target! We need a path, and we need to reset our persistency, if needed. + new Float:flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_init_newtarget", -1.0); + if (flPersistencyTime >= 0.0) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + flPersistencyTime; + } + + flPersistencyTime = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_newtarget", 2.0); + if (flPersistencyTime >= 0.0) + { + if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); + g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTime; + } + + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); + bQueueForNewPath = true; // Brand new target! We need a new path! + } + else if ((bPlayerInFOV[iTarget] && bPlayerVisible[iTarget]) || GetGameTime() < g_flSlenderTimeUntilNoPersistence[iBossIndex]) + { + // Constantly update my path if I see him or if I'm still being persistent. + if (GetGameTime() >= g_flSlenderNextPathTime[iBossIndex]) + { + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", g_flSlenderGoalPos[iBossIndex]); + bQueueForNewPath = true; + g_flSlenderNextPathTime[iBossIndex] = GetGameTime() + 0.33; + } + } + } + } + + if (NavMesh_Exists()) + { + // So by now we should have calculated our master goal position. + // Now we use that to create a path. + + if (bQueueForNewPath) + { + ClearArray(g_hSlenderPath[iBossIndex]); + + new iCurrentAreaIndex = NavMesh_GetNearestArea(flMyPos); + if (iCurrentAreaIndex != -1) + { + new iGoalAreaIndex = NavMesh_GetNearestArea(g_flSlenderGoalPos[iBossIndex]); + if (iGoalAreaIndex != -1) + { + decl Float:flCenter[3], Float:flCenterPortal[3], Float:flClosestPoint[3]; + new iClosestAreaIndex = 0; + + new bool:bPathSuccess = NavMesh_BuildPath(iCurrentAreaIndex, + iGoalAreaIndex, + g_flSlenderGoalPos[iBossIndex], + SlenderChaseBossShortestPathCost, + RoundToFloor(NPCChaserGetStepSize(iBossIndex)), + iClosestAreaIndex); + + new iTempAreaIndex = iClosestAreaIndex; + new iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); + new iNavDirection; + new Float:flHalfWidth; + + if (bPathSuccess) + { + // Path successful? Insert the goal position into our list. + new iIndex = PushArrayCell(g_hSlenderPath[iBossIndex], g_flSlenderGoalPos[iBossIndex][0]); + SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, g_flSlenderGoalPos[iBossIndex][1], 1); + SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, g_flSlenderGoalPos[iBossIndex][2], 2); + } + + while (iTempParentAreaIndex != -1) + { + // Build a path of waypoints along the nav mesh for our AI to follow. + // Path order is first come, first served, so when we got our waypoint list, + // we have to reverse it so that the starting waypoint would be in front. + + NavMeshArea_GetCenter(iTempParentAreaIndex, flCenter); + iNavDirection = NavMeshArea_ComputeDirection(iTempAreaIndex, flCenter); + NavMeshArea_ComputePortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flHalfWidth); + NavMeshArea_ComputeClosestPointInPortal(iTempAreaIndex, iTempParentAreaIndex, iNavDirection, flCenterPortal, flClosestPoint); + + flClosestPoint[2] = NavMeshArea_GetZ(iTempAreaIndex, flClosestPoint); + + new iIndex = PushArrayCell(g_hSlenderPath[iBossIndex], flClosestPoint[0]); + SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, flClosestPoint[1], 1); + SetArrayCell(g_hSlenderPath[iBossIndex], iIndex, flClosestPoint[2], 2); + + iTempAreaIndex = iTempParentAreaIndex; + iTempParentAreaIndex = NavMeshArea_GetParent(iTempAreaIndex); + } + + // Set our goal position to the start node (hopefully there's something in the array). + if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) + { + new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; + + g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); + g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); + g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); + } + } + else + { + PrintToServer("SF2: Failed to create new path for boss %d: destination is not on nav mesh!", iBossIndex); + } + } + else + { + PrintToServer("SF2: Failed to create new path for boss %d: boss is not on nav mesh!", iBossIndex); + } + } + } + else + { + // The nav mesh doesn't exist? Well, that sucks. + ClearArray(g_hSlenderPath[iBossIndex]); + } + + if (iState == STATE_CHASE || iState == STATE_ATTACK) + { + if (IsValidClient(iTarget)) + { +#if defined DEBUG + SendDebugMessageToPlayer(iTarget, DEBUG_BOSS_CHASE, 1, "g_flSlenderTimeUntilAlert[%d]: %f\ng_flSlenderTimeUntilNoPersistence[%d]: %f", iBossIndex, g_flSlenderTimeUntilAlert[iBossIndex] - GetGameTime(), iBossIndex, g_flSlenderTimeUntilNoPersistence[iBossIndex] - GetGameTime()); +#endif + + if (bPlayerInFOV[iTarget] && bPlayerVisible[iTarget]) + { + new Float:flDistRatio = flPlayerDists[iTarget] / NPCGetSearchRadius(iBossIndex); + + new Float:flChaseDurationTimeAddMin = GetProfileFloat(sSlenderProfile, "search_chase_duration_add_visible_min", 0.025); + new Float:flChaseDurationTimeAddMax = GetProfileFloat(sSlenderProfile, "search_chase_duration_add_visible_max", 0.2); + + new Float:flChaseDurationAdd = flChaseDurationTimeAddMax - ((flChaseDurationTimeAddMax - flChaseDurationTimeAddMin) * flDistRatio); + + if (flChaseDurationAdd > 0.0) + { + g_flSlenderTimeUntilAlert[iBossIndex] += flChaseDurationAdd; + if (g_flSlenderTimeUntilAlert[iBossIndex] > (GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"))) + { + g_flSlenderTimeUntilAlert[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"); + } + } + + new Float:flPersistencyTimeAddMin = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_visible_min", 0.05); + new Float:flPersistencyTimeAddMax = GetProfileFloat(sSlenderProfile, "search_chase_persistency_time_add_visible_max", 0.15); + + new Float:flPersistencyTimeAdd = flPersistencyTimeAddMax - ((flPersistencyTimeAddMax - flPersistencyTimeAddMin) * flDistRatio); + + if (flPersistencyTimeAdd > 0.0) + { + if (g_flSlenderTimeUntilNoPersistence[iBossIndex] < GetGameTime()) g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime(); + + g_flSlenderTimeUntilNoPersistence[iBossIndex] += flPersistencyTimeAdd; + if (g_flSlenderTimeUntilNoPersistence[iBossIndex] > (GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"))) + { + g_flSlenderTimeUntilNoPersistence[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "search_chase_duration"); + } + } + } + } + } + + // Process through our path waypoints. + if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) + { + decl Float:flHitNormal[3]; + decl Float:flNodePos[3]; + + new Float:flNodeToleranceDist = g_flSlenderPathNodeTolerance[iBossIndex]; + new bool:bGotNewPoint = false; + + for (new iNodeIndex = 0, iNodeCount = GetArraySize(g_hSlenderPath[iBossIndex]); iNodeIndex < iNodeCount; iNodeIndex++) + { + flNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 0); + flNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 1); + flNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeIndex, 2); + + new Handle:hTrace = TR_TraceHullFilterEx(flMyPos, + flNodePos, + flSlenderMins, + flSlenderMaxs, + MASK_NPCSOLID, + TraceRayDontHitCharactersOrEntity, + slender); + + new bool:bDidHit = TR_DidHit(hTrace); + TR_GetPlaneNormal(hTrace, flHitNormal); + CloseHandle(hTrace); + GetVectorAngles(flHitNormal, flHitNormal); + for (new i = 0; i < 3; i++) flHitNormal[i] = AngleNormalize(flHitNormal[i]); + + // First check if we can see the point. + if (!bDidHit || ((flHitNormal[0] >= 0.0 && flHitNormal[0] > 45.0) || (flHitNormal[0] < 0.0 && flHitNormal[0] < -45.0))) + { + new bool:bNearNode = false; + + // See if we're already near enough. + new Float:flDist = GetVectorDistance(flNodePos, flMyPos); + if (flDist < flNodeToleranceDist) bNearNode = true; + + if (!bNearNode) + { + new bool:bOutside = false; + + // Then, predict if we're going to pass over the point on the next think. + decl Float:flTestPos[3]; + NormalizeVector(flSlenderVelocity, flTestPos); + ScaleVector(flTestPos, GetVectorLength(flSlenderVelocity) * BOSS_THINKRATE); + AddVectors(flMyPos, flTestPos, flTestPos); + + decl Float:flP[3], Float:flS[3]; + SubtractVectors(flNodePos, flMyPos, flP); + SubtractVectors(flTestPos, flMyPos, flS); + + new Float:flSP = GetVectorDotProduct(flP, flS); + if (flSP <= 0.0) bOutside = true; + + new Float:flPP = GetVectorDotProduct(flS, flS); + + if (!bOutside) + { + if (flPP <= flSP) bOutside = true; + } + + if (!bOutside) + { + decl Float:flD[3]; + ScaleVector(flS, (flSP / flPP)); + SubtractVectors(flP, flS, flD); + + flDist = GetVectorLength(flD); + if (flDist < flNodeToleranceDist) + { + bNearNode = true; + } + } + } + + if (bNearNode) + { + // Shave off this node and set our goal position to the next one. + + ResizeArray(g_hSlenderPath[iBossIndex], iNodeIndex); + + if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) + { + new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; + + g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); + g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); + g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); + } + + bGotNewPoint = true; + break; + } + } + } + + if (!bGotNewPoint) + { + // Try to see if we can look ahead. + + decl Float:flMyEyePos[3]; + NPCGetEyePosition(iBossIndex, flMyEyePos); + + new Float:flNodeLookAheadDist = g_flSlenderPathNodeLookAhead[iBossIndex]; + if (flNodeLookAheadDist > 0.0) + { + new iNodeCount = GetArraySize(g_hSlenderPath[iBossIndex]); + if (iNodeCount) + { + decl Float:flInitDir[3]; + flInitDir[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 0); + flInitDir[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 1); + flInitDir[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iNodeCount - 1, 2); + + SubtractVectors(flInitDir, flMyPos, flInitDir); + NormalizeVector(flInitDir, flInitDir); + + decl Float:flPrevDir[3]; + flPrevDir[0] = flInitDir[0]; + flPrevDir[1] = flInitDir[1]; + flPrevDir[2] = flInitDir[2]; + + NormalizeVector(flPrevDir, flPrevDir); + + decl Float:flPrevNodePos[3]; + + new iStartPointIndex = iNodeCount - 1; + new Float:flRangeSoFar = 0.0; + + new iLookAheadPointIndex; + for (iLookAheadPointIndex = iStartPointIndex; iLookAheadPointIndex >= 0; iLookAheadPointIndex--) + { + flNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 0); + flNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 1); + flNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex, 2); + + decl Float:flDir[3]; + if (iLookAheadPointIndex == iStartPointIndex) + { + SubtractVectors(flNodePos, flMyPos, flDir); + NormalizeVector(flDir, flDir); + } + else + { + flPrevNodePos[0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 0); + flPrevNodePos[1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 1); + flPrevNodePos[2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1, 2); + + SubtractVectors(flNodePos, flPrevNodePos, flDir); + NormalizeVector(flDir, flDir); + } + + if (GetVectorDotProduct(flDir, flInitDir) < 0.0) + { + break; + } + + if (GetVectorDotProduct(flDir, flPrevDir) < 0.5) + { + break; + } + + flPrevDir[0] = flDir[0]; + flPrevDir[1] = flDir[1]; + flPrevDir[2] = flDir[2]; + + decl Float:flProbe[3]; + flProbe[0] = flNodePos[0]; + flProbe[1] = flNodePos[1]; + flProbe[2] = flNodePos[2] + HalfHumanHeight; + + if (!IsWalkableTraceLineClear(flMyEyePos, flProbe, WALK_THRU_BREAKABLES)) + { + break; + } + + if (iLookAheadPointIndex == iStartPointIndex) + { + flRangeSoFar += GetVectorDistance(flMyPos, flNodePos); + } + else + { + flRangeSoFar += GetVectorDistance(flNodePos, flPrevNodePos); + } + + if (flRangeSoFar >= flNodeLookAheadDist) + { + break; + } + } + + // Shave off all unnecessary nodes and keep the one that is within + // our viewsight. + + ResizeArray(g_hSlenderPath[iBossIndex], iLookAheadPointIndex + 1); + + if (GetArraySize(g_hSlenderPath[iBossIndex]) > 0) + { + new iPosIndex = GetArraySize(g_hSlenderPath[iBossIndex]) - 1; + + g_flSlenderGoalPos[iBossIndex][0] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 0); + g_flSlenderGoalPos[iBossIndex][1] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 1); + g_flSlenderGoalPos[iBossIndex][2] = Float:GetArrayCell(g_hSlenderPath[iBossIndex], iPosIndex, 2); + } + + bGotNewPoint = true; + } + } + } + } + + if (iState != STATE_ATTACK && iState != STATE_STUN) + { + // Animation playback speed handling. + if (iModel && iModel != INVALID_ENT_REFERENCE) + { + if (iState == STATE_WANDER && !(NPCGetFlags(iBossIndex) & SFF_WANDERMOVE)) + { + SetVariantFloat(flPlaybackRateIdle); + AcceptEntityInput(iModel, "SetPlaybackRate"); + } + else + { + SetVariantFloat(iState == STATE_CHASE ? (flVelocityRatio * flPlaybackRateRun) : (flVelocityRatioWalk * flPlaybackRateWalk)); + AcceptEntityInput(iModel, "SetPlaybackRate"); + } + } + } + } + } + + // Sound handling. + if (GetGameTime() >= g_flSlenderNextVoiceSound[iBossIndex]) + { + if (iState == STATE_IDLE || iState == STATE_WANDER) + { + SlenderPerformVoice(iBossIndex, "sound_idle"); + } + else if (iState == STATE_ALERT) + { + SlenderPerformVoice(iBossIndex, "sound_alertofenemy"); + } + else if (iState == STATE_CHASE || iState == STATE_ATTACK) + { + SlenderPerformVoice(iBossIndex, "sound_chasingenemy"); + } + } + + // Reset our interrupt conditions. + g_iSlenderInterruptConditions[iBossIndex] = 0; + + return Plugin_Continue; +} + +SlenderChaseBossProcessMovement(iBossIndex) +{ + new iBoss = NPCGetEntIndex(iBossIndex); + new iState = g_iSlenderState[iBossIndex]; + + // Constantly set the monster_generic's NPC state to idle to prevent + // velocity confliction. + + SetEntProp(iBoss, Prop_Data, "m_NPCState", 0); + + new Float:flWalkSpeed = g_flSlenderCalculatedWalkSpeed[iBossIndex]; + new Float:flSpeed = g_flSlenderCalculatedSpeed[iBossIndex]; + + new Float:flMyPos[3], Float:flMyEyeAng[3], Float:flMyVelocity[3]; + + decl String:sSlenderProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sSlenderProfile, sizeof(sSlenderProfile)); + + GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flMyPos); + GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); + GetEntPropVector(iBoss, Prop_Data, "m_vecAbsVelocity", flMyVelocity); + + decl Float:flBossMins[3], Float:flBossMaxs[3]; + GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flBossMins); + GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flBossMaxs); + + decl Float:flTraceMins[3], Float:flTraceMaxs[3]; + flTraceMins[0] = flBossMins[0]; + flTraceMins[1] = flBossMins[1]; + flTraceMins[2] = 0.0; + flTraceMaxs[0] = flBossMaxs[0]; + flTraceMaxs[1] = flBossMaxs[1]; + flTraceMaxs[2] = 0.0; + + // By now we should have our preferable goal position. Initiate + // reflex adjustments. + + g_bSlenderFeelerReflexAdjustment[iBossIndex] = false; + + { + decl Float:flMoveDir[3]; + NormalizeVector(flMyVelocity, flMoveDir); + flMoveDir[2] = 0.0; + + decl Float:flLat[3]; + flLat[0] = -flMoveDir[1]; + flLat[1] = flMoveDir[0]; + flLat[2] = 0.0; + + new Float:flFeelerOffset = 25.0; + new Float:flFeelerLengthRun = 50.0; + new Float:flFeelerLengthWalk = 30.0; + new Float:flFeelerHeight = StepHeight + 0.1; + + new Float:flFeelerLength = iState == STATE_CHASE ? flFeelerLengthRun : flFeelerLengthWalk; + + // Get the ground height and normal. + new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, Float:{ 0.0, 0.0, 90.0 }, MASK_NPCSOLID, RayType_Infinite, TraceFilterWalkableEntities); + decl Float:flTraceEndPos[3]; + decl Float:flTraceNormal[3]; + TR_GetEndPosition(flTraceEndPos, hTrace); + TR_GetPlaneNormal(hTrace, flTraceNormal); + new bool:bTraceHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bTraceHit) + { + //new Float:flGroundHeight = GetVectorDistance(flMyPos, flTraceEndPos); + NormalizeVector(flTraceNormal, flTraceNormal); + GetVectorCrossProduct(flLat, flTraceNormal, flMoveDir); + GetVectorCrossProduct(flMoveDir, flTraceNormal, flLat); + + decl Float:flFeet[3]; + flFeet[0] = flMyPos[0]; + flFeet[1] = flMyPos[1]; + flFeet[2] = flMyPos[2] + flFeelerHeight; + + decl Float:flTo[3]; + decl Float:flFrom[3]; + for (new i = 0; i < 3; i++) + { + flFrom[i] = flFeet[i] + (flFeelerOffset * flLat[i]); + flTo[i] = flFrom[i] + (flFeelerLength * flMoveDir[i]); + } + + new bool:bLeftClear = IsWalkableTraceLineClear(flFrom, flTo, WALK_THRU_DOORS | WALK_THRU_BREAKABLES); + + for (new i = 0; i < 3; i++) + { + flFrom[i] = flFeet[i] - (flFeelerOffset * flLat[i]); + flTo[i] = flFrom[i] + (flFeelerLength * flMoveDir[i]); + } + + new bool:bRightClear = IsWalkableTraceLineClear(flFrom, flTo, WALK_THRU_DOORS | WALK_THRU_BREAKABLES); + + new Float:flAvoidRange = 300.0; + + if (!bRightClear) + { + if (bLeftClear) + { + g_bSlenderFeelerReflexAdjustment[iBossIndex] = true; + + for (new i = 0; i < 3; i++) + { + g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i] = g_flSlenderGoalPos[iBossIndex][i] + (flAvoidRange * flLat[i]); + } + } + } + else if (!bLeftClear) + { + g_bSlenderFeelerReflexAdjustment[iBossIndex] = true; + + for (new i = 0; i < 3; i++) + { + g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i] = g_flSlenderGoalPos[iBossIndex][i] - (flAvoidRange * flLat[i]); + } + } + } + } + + new Float:flGoalPosition[3]; + if (g_bSlenderFeelerReflexAdjustment[iBossIndex]) + { + for (new i = 0; i < 3; i++) + { + flGoalPosition[i] = g_flSlenderFeelerReflexAdjustmentPos[iBossIndex][i]; + } + } + else + { + for (new i = 0; i < 3; i++) + { + flGoalPosition[i] = g_flSlenderGoalPos[iBossIndex][i]; + } + } + + // Process our desired velocity. + new Float:flDesiredVelocity[3]; + switch (iState) + { + case STATE_WANDER: + { + if (NPCGetFlags(iBossIndex) & SFF_WANDERMOVE) + { + SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); + flDesiredVelocity[2] = 0.0; + NormalizeVector(flDesiredVelocity, flDesiredVelocity); + ScaleVector(flDesiredVelocity, flWalkSpeed); + } + } + case STATE_ALERT: + { + SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); + flDesiredVelocity[2] = 0.0; + NormalizeVector(flDesiredVelocity, flDesiredVelocity); + ScaleVector(flDesiredVelocity, flWalkSpeed); + } + case STATE_CHASE: + { + SubtractVectors(flGoalPosition, flMyPos, flDesiredVelocity); + flDesiredVelocity[2] = 0.0; + NormalizeVector(flDesiredVelocity, flDesiredVelocity); + ScaleVector(flDesiredVelocity, flSpeed); + } + } + + // Check if we're on the ground. + new bool:bSlenderOnGround = bool:(GetEntityFlags(iBoss) & FL_ONGROUND); + + decl Float:flTraceEndPos[3]; + new Handle:hTrace; + + // Determine speed behavior. + if (bSlenderOnGround) + { + // Don't change the speed behavior. + } + else + { + flDesiredVelocity[2] = 0.0; + NormalizeVector(flDesiredVelocity, flDesiredVelocity); + ScaleVector(flDesiredVelocity, NPCChaserGetAirSpeed(iBossIndex, GetConVarInt(g_cvDifficulty))); + } + + new bool:bSlenderTeleportedOnStep = false; + new Float:flSlenderStepSize = NPCChaserGetStepSize(iBossIndex); + + // Check our stepsize in case we need to elevate ourselves a step. + if (bSlenderOnGround && GetVectorLength(flDesiredVelocity) > 0.0) + { + if (flSlenderStepSize > 0.0) + { + decl Float:flTraceDirection[3], Float:flObstaclePos[3], Float:flObstacleNormal[3]; + NormalizeVector(flDesiredVelocity, flTraceDirection); + AddVectors(flMyPos, flTraceDirection, flTraceEndPos); + + // Tracehull in front of us to check if there's a very small obstacle blocking our way. + hTrace = TR_TraceHullFilterEx(flMyPos, + flTraceEndPos, + flBossMins, + flBossMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + new bool:bSlenderHitObstacle = TR_DidHit(hTrace); + TR_GetEndPosition(flObstaclePos, hTrace); + TR_GetPlaneNormal(hTrace, flObstacleNormal); + CloseHandle(hTrace); + + if (bSlenderHitObstacle && + FloatAbs(flObstacleNormal[2]) == 0.0) + { + decl Float:flTraceStartPos[3]; + flTraceStartPos[0] = flObstaclePos[0]; + flTraceStartPos[1] = flObstaclePos[1]; + + decl Float:flTraceFreePos[3]; + + new Float:flTraceCheckZ = 0.0; + + // This does a crapload of traces along the wall. Very nasty and expensive to do... + while (flTraceCheckZ <= flSlenderStepSize) + { + flTraceCheckZ += 1.0; + flTraceStartPos[2] = flObstaclePos[2] + flTraceCheckZ; + + AddVectors(flTraceStartPos, flTraceDirection, flTraceEndPos); + + hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flTraceEndPos, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + bSlenderHitObstacle = TR_DidHit(hTrace); + TR_GetEndPosition(flTraceFreePos, hTrace); + CloseHandle(hTrace); + + if (!bSlenderHitObstacle) + { + // Potential space to step on? See if we can fit! + if (!IsSpaceOccupiedNPC(flTraceFreePos, + flBossMins, + flBossMaxs, + iBoss)) + { + // Yes we can! Break the loop and teleport to this pos. + bSlenderTeleportedOnStep = true; + TeleportEntity(iBoss, flTraceFreePos, NULL_VECTOR, NULL_VECTOR); + break; + } + } + } + } + /* + else if (!bSlenderHitObstacle) + { + decl Float:flTraceStartPos[3]; + flTraceStartPos[0] = flObstaclePos[0]; + flTraceStartPos[1] = flObstaclePos[1]; + + decl Float:flTraceFreePos[3]; + + new Float:flTraceCheckZ = 0.0; + + // This does a crapload of traces along the wall. Very nasty and expensive to do... + while (flTraceCheckZ <= flSlenderStepSize) + { + flTraceCheckZ += 1.0; + flTraceStartPos[2] = flObstaclePos[2] - flTraceCheckZ; + + AddVectors(flTraceStartPos, flTraceDirection, flTraceEndPos); + + hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flTraceEndPos, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + bSlenderHitObstacle = TR_DidHit(hTrace); + TR_GetEndPosition(flTraceFreePos, hTrace); + CloseHandle(hTrace); + + if (bSlenderHitObstacle) + { + // Potential space to step on? See if we can fit! + if (!IsSpaceOccupiedNPC(flTraceFreePos, + flBossMins, + flBossMaxs, + iBoss)) + { + // Yes we can! Break the loop and teleport to this pos. + bSlenderTeleportedOnStep = true; + TeleportEntity(iBoss, flTraceFreePos, NULL_VECTOR, NULL_VECTOR); + break; + } + } + } + } + */ + } + } + + // Apply acceleration vectors. + new Float:flMoveVelocity[3]; + new Float:flFrameTime = GetTickInterval(); + decl Float:flAcceleration[3]; + SubtractVectors(flDesiredVelocity, flMyVelocity, flAcceleration); + NormalizeVector(flAcceleration, flAcceleration); + ScaleVector(flAcceleration, g_flSlenderAcceleration[iBossIndex] * flFrameTime); + + AddVectors(flMyVelocity, flAcceleration, flMoveVelocity); + + new Float:flSlenderJumpSpeed = g_flSlenderJumpSpeed[iBossIndex]; + new bool:bSlenderShouldJump = false; + + decl Float:angJumpReach[3]; + + // Check if we need to jump over a wall or something. + if (!bSlenderShouldJump && bSlenderOnGround && !bSlenderTeleportedOnStep && flSlenderJumpSpeed > 0.0 && GetVectorLength(flDesiredVelocity) > 0.0 && + GetGameTime() >= g_flSlenderNextJump[iBossIndex]) + { + new Float:flZDiff = (flGoalPosition[2] - flMyPos[2]); + + if (flZDiff > flSlenderStepSize) + { + // Our path has a jump thingy to it. Calculate the jump height needed to reach it and how far away we should start + // checking on when to jump. + + decl Float:vecDir[3], Float:vecDesiredDir[3]; + GetVectorAngles(flMyVelocity, vecDir); + SubtractVectors(flGoalPosition, flMyPos, vecDesiredDir); + GetVectorAngles(vecDesiredDir, vecDesiredDir); + + if ((FloatAbs(AngleDiff(vecDir[0], vecDesiredDir[0])) + FloatAbs(AngleDiff(vecDir[1], vecDesiredDir[1]))) >= 45.0) + { + // Assuming we are actually capable of making the jump, find out WHEN we have to jump, + // based on 2D distance between our position and the target point, and our current horizontal + // velocity. + + decl Float:vecMyPos2D[3], Float:vecGoalPos2D[3]; + vecMyPos2D[0] = flMyPos[0]; + vecMyPos2D[1] = flMyPos[1]; + vecMyPos2D[2] = 0.0; + vecGoalPos2D[0] = flGoalPosition[0]; + vecGoalPos2D[1] = flGoalPosition[1]; + vecGoalPos2D[2] = 0.0; + + new Float:fl2DDist = GetVectorDistance(vecMyPos2D, vecGoalPos2D); + + new Float:flNotImaginary = Pow(flSlenderJumpSpeed, 4.0) - (g_flGravity * (g_flGravity * Pow(fl2DDist, 2.0)) + (2.0 * flZDiff * Pow(flSlenderJumpSpeed, 2.0))); + if (flNotImaginary >= 0.0) + { + // We can reach it. + new Float:flNotInfinite = g_flGravity * fl2DDist; + if (flNotInfinite > 0.0) + { + SubtractVectors(vecGoalPos2D, vecMyPos2D, angJumpReach); + GetVectorAngles(angJumpReach, angJumpReach); + angJumpReach[0] = -RadToDeg(ArcTangent((Pow(flSlenderJumpSpeed, 2.0) + SquareRoot(flNotImaginary)) / flNotInfinite)); + bSlenderShouldJump = true; + } + } + } + } + } + + if (bSlenderOnGround && bSlenderShouldJump) + { + g_flSlenderNextJump[iBossIndex] = GetGameTime() + GetProfileFloat(sSlenderProfile, "jump_cooldown", 2.0); + + decl Float:vecJump[3]; + GetAngleVectors(angJumpReach, vecJump, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(vecJump, vecJump); + ScaleVector(vecJump, flSlenderJumpSpeed); + AddVectors(flMoveVelocity, vecJump, flMoveVelocity); + } + else + { + // We are in no position to defy gravity. + flMoveVelocity[2] = flMyVelocity[2]; + } + + decl Float:flMoveAng[3]; + new bool:bChangeAngles = false; + + // Process angles. + if (iState != STATE_ATTACK && iState != STATE_STUN) + { + if (NPCHasAttribute(iBossIndex, "always look at target")) + { + new iTarget = EntRefToEntIndex(g_iSlenderTarget[iBossIndex]); + + if (iTarget && iTarget != INVALID_ENT_REFERENCE) + { + decl Float:flTargetPos[3]; + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTargetPos); + SubtractVectors(flTargetPos, flMyPos, flMoveAng); + GetVectorAngles(flMoveAng, flMoveAng); + } + else + { + SubtractVectors(flGoalPosition, flMyPos, flMoveAng); + GetVectorAngles(flMoveAng, flMoveAng); + } + } + else + { + SubtractVectors(flGoalPosition, flMyPos, flMoveAng); + GetVectorAngles(flMoveAng, flMoveAng); + } + + new Float:flTurnRate = NPCGetTurnRate(iBossIndex); + if (iState == STATE_CHASE) flTurnRate *= 2.0; + + flMoveAng[0] = 0.0; + flMoveAng[2] = 0.0; + flMoveAng[1] = ApproachAngle(flMoveAng[1], flMyEyeAng[1], flTurnRate * flFrameTime); + + bChangeAngles = true; + } + + TeleportEntity(iBoss, NULL_VECTOR, bChangeAngles ? flMoveAng : NULL_VECTOR, flMoveVelocity); +} + +// Shortest-path cost function for NavMesh_BuildPath. +public SlenderChaseBossShortestPathCost(iAreaIndex, iFromAreaIndex, iLadderIndex, any:iStepSize) +{ + if (iFromAreaIndex == -1) + { + return 0; + } + else + { + new iDist; + decl Float:flAreaCenter[3], Float:flFromAreaCenter[3]; + NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); + NavMeshArea_GetCenter(iFromAreaIndex, flFromAreaCenter); + + if (iLadderIndex != -1) + { + iDist = RoundFloat(NavMeshLadder_GetLength(iLadderIndex)); + } + else + { + iDist = RoundFloat(GetVectorDistance(flAreaCenter, flFromAreaCenter)); + } + + new iCost = iDist + NavMeshArea_GetCostSoFar(iFromAreaIndex); + + new iAreaFlags = NavMeshArea_GetFlags(iAreaIndex); + if (iAreaFlags & NAV_MESH_CROUCH) iCost += 20; + if (iAreaFlags & NAV_MESH_JUMP) iCost += (5 * iDist); + + if ((flAreaCenter[2] - flFromAreaCenter[2]) > iStepSize) iCost += iStepSize; + + return iCost; + } +} + +public Action:Timer_SlenderChaseBossAttack(Handle:timer, any:entref) +{ + if (!g_bEnabled) return; + + new slender = EntRefToEntIndex(entref); + if (!slender || slender == INVALID_ENT_REFERENCE) return; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return; + + if (timer != g_hSlenderAttackTimer[iBossIndex]) return; + + if (NPCGetFlags(iBossIndex) & SFF_FAKE) + { + SlenderMarkAsFake(iBossIndex); + return; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new bool:bAttackEliminated = bool:(NPCGetFlags(iBossIndex) & SFF_ATTACKWAITERS); + + new Float:flDamage = NPCChaserGetAttackDamage(iBossIndex, 0); + new Float:flDamageVsProps = NPCChaserGetAttackDamageVsProps(iBossIndex, 0); + new iDamageType = NPCChaserGetAttackDamageType(iBossIndex, 0); + + // Damage all players within range. + decl Float:flMyEyePos[3], Float:flMyEyeAng[3]; + NPCGetEyePosition(iBossIndex, flMyEyePos); + GetEntPropVector(slender, Prop_Data, "m_angAbsRotation", flMyEyeAng); + AddVectors(g_flSlenderEyePosOffset[iBossIndex], flMyEyeAng, flMyEyeAng); + for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); + + decl Float:flViewPunch[3]; + GetProfileVector(sProfile, "attack_punchvel", flViewPunch); + + decl Float:flTargetDist; + decl Handle:hTrace; + + new Float:flAttackRange = NPCChaserGetAttackRange(iBossIndex, 0); + new Float:flAttackFOV = NPCChaserGetAttackSpread(iBossIndex, 0); + new Float:flAttackDamageForce = NPCChaserGetAttackDamageForce(iBossIndex, 0); + + new bool:bHit = false; + + { + new prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_physics")) != -1) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bHit = true; + SDKHooks_TakeDamage(prop, slender, slender, flDamageVsProps, iDamageType, _, _, flMyEyePos); + } + } + + prop = -1; + while ((prop = FindEntityByClassname(prop, "prop_dynamic")) != -1) + { + if (GetEntProp(prop, Prop_Data, "m_iHealth") > 0) + { + if (NPCAttackValidateTarget(iBossIndex, prop, flAttackRange, flAttackFOV)) + { + bHit = true; + SDKHooks_TakeDamage(prop, slender, slender, flDamageVsProps, iDamageType, _, _, flMyEyePos); + } + } + } + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInGhostMode(i)) continue; + + if (!bAttackEliminated && g_bPlayerEliminated[i]) continue; + + decl Float:flTargetPos[3]; + GetClientEyePosition(i, flTargetPos); + + hTrace = TR_TraceRayFilterEx(flMyEyePos, + flTargetPos, + MASK_NPCSOLID, + RayType_EndPoint, + TraceRayDontHitEntity, + slender); + + new bool:bTraceDidHit = TR_DidHit(hTrace); + new iTraceHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (bTraceDidHit && iTraceHitEntity != i) + { + decl Float:flTargetMins[3], Float:flTargetMaxs[3]; + GetEntPropVector(i, Prop_Send, "m_vecMins", flTargetMins); + GetEntPropVector(i, Prop_Send, "m_vecMaxs", flTargetMaxs); + GetClientAbsOrigin(i, flTargetPos); + for (new i2 = 0; i2 < 3; i2++) flTargetPos[i2] += ((flTargetMins[i2] + flTargetMaxs[i2]) / 2.0); + + hTrace = TR_TraceRayFilterEx(flMyEyePos, + flTargetPos, + MASK_NPCSOLID, + RayType_EndPoint, + TraceRayDontHitEntity, + slender); + + bTraceDidHit = TR_DidHit(hTrace); + iTraceHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + } + + if (!bTraceDidHit || iTraceHitEntity == i) + { + flTargetDist = GetVectorDistance(flTargetPos, flMyEyePos); + + if (flTargetDist <= flAttackRange) + { + decl Float:flDirection[3]; + SubtractVectors(flTargetPos, flMyEyePos, flDirection); + GetVectorAngles(flDirection, flDirection); + + if (FloatAbs(AngleDiff(flDirection[1], flMyEyeAng[1])) <= flAttackFOV) + { + bHit = true; + GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flDirection, flDirection); + ScaleVector(flDirection, flAttackDamageForce); + + Call_StartForward(fOnClientDamagedByBoss); + Call_PushCell(i); + Call_PushCell(iBossIndex); + Call_PushCell(slender); + Call_PushFloat(flDamage); + Call_PushCell(iDamageType); + Call_Finish(); + + SDKHooks_TakeDamage(i, slender, slender, flDamage, iDamageType, _, flDirection, flMyEyePos); + ClientViewPunch(i, flViewPunch); + + if (NPCHasAttribute(iBossIndex, "bleed player on hit")) + { + new Float:flDuration = NPCGetAttributeValue(iBossIndex, "bleed player on hit"); + if (flDuration > 0.0) + { + TF2_MakeBleed(i, slender, flDuration); + } + } + + // Add stress + new Float:flStressScalar = flDamage / 125.0; + if (flStressScalar > 1.0) flStressScalar = 1.0; + ClientAddStress(i, 0.33 * flStressScalar); + } + } + } + } + + decl String:sSoundPath[PLATFORM_MAX_PATH]; + + if (bHit) + { + // Fling it. + new phys = CreateEntityByName("env_physexplosion"); + if (phys != -1) + { + TeleportEntity(phys, flMyEyePos, NULL_VECTOR, NULL_VECTOR); + DispatchKeyValue(phys, "spawnflags", "1"); + DispatchKeyValueFloat(phys, "radius", flAttackRange); + DispatchKeyValueFloat(phys, "magnitude", flAttackDamageForce); + DispatchSpawn(phys); + ActivateEntity(phys); + AcceptEntityInput(phys, "Explode"); + AcceptEntityInput(phys, "Kill"); + } + + GetRandomStringFromProfile(sProfile, "sound_hitenemy", sSoundPath, sizeof(sSoundPath)); + if (sSoundPath[0]) EmitSoundToAll(sSoundPath, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + } + else + { + GetRandomStringFromProfile(sProfile, "sound_missenemy", sSoundPath, sizeof(sSoundPath)); + if (sSoundPath[0]) EmitSoundToAll(sSoundPath, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + } + + g_hSlenderAttackTimer[iBossIndex] = CreateTimer(GetProfileFloat(sProfile, "attack_endafter"), Timer_SlenderChaseBossAttackEnd, entref, TIMER_FLAG_NO_MAPCHANGE); +} + +static NPCAttackValidateTarget(iBossIndex, iTarget, Float:flAttackRange, Float:flAttackFOV) +{ + new iBoss = NPCGetEntIndex(iBossIndex); + + decl Float:flMyEyePos[3], Float:flMyEyeAng[3]; + NPCGetEyePosition(iBossIndex, flMyEyePos); + GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); + AddVectors(g_flSlenderEyeAngOffset[iBossIndex], flMyEyeAng, flMyEyeAng); + for (new i = 0; i < 3; i++) flMyEyeAng[i] = AngleNormalize(flMyEyeAng[i]); + + decl Float:flTargetPos[3], Float:flTargetMins[3], Float:flTargetMaxs[3]; + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flTargetPos); + GetEntPropVector(iTarget, Prop_Send, "m_vecMins", flTargetMins); + GetEntPropVector(iTarget, Prop_Send, "m_vecMaxs", flTargetMaxs); + + for (new i = 0; i < 3; i++) + { + flTargetPos[i] += (flTargetMins[i] + flTargetMaxs[i]) / 2.0; + } + + new Float:flTargetDist = GetVectorDistance(flTargetPos, flMyEyePos); + if (flTargetDist <= flAttackRange) + { + decl Float:flDirection[3]; + SubtractVectors(g_flSlenderGoalPos[iBossIndex], flMyEyePos, flDirection); + GetVectorAngles(flDirection, flDirection); + + if (FloatAbs(AngleDiff(flDirection[1], flMyEyeAng[1])) <= flAttackFOV / 2.0) + { + new Handle:hTrace = TR_TraceRayFilterEx(flMyEyePos, + flTargetPos, + MASK_NPCSOLID, + RayType_EndPoint, + TraceRayDontHitEntity, + iBoss); + + new bool:bTraceDidHit = TR_DidHit(hTrace); + new iTraceHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (!bTraceDidHit || iTraceHitEntity == iTarget) + { + return true; + } + } + } + + return false; +} + +public Action:Timer_SlenderChaseBossAttackEnd(Handle:timer, any:entref) +{ + if (!g_bEnabled) return; + + new slender = EntRefToEntIndex(entref); + if (!slender || slender == INVALID_ENT_REFERENCE) return; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return; + + if (timer != g_hSlenderAttackTimer[iBossIndex]) return; + + g_bSlenderAttacking[iBossIndex] = false; + g_hSlenderAttackTimer[iBossIndex] = INVALID_HANDLE; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/playergroups.sp b/addons/sourcemod/scripting/rytp_horror/playergroups.sp index 8a6ea0e..27b65d5 100644 --- a/addons/sourcemod/scripting/rytp_horror/playergroups.sp +++ b/addons/sourcemod/scripting/rytp_horror/playergroups.sp @@ -1,614 +1,614 @@ -#if defined _sf2_playergroups_included - #endinput -#endif -#define _sf2_playergroups_included - -#define SF2_MAX_PLAYER_GROUPS MAXPLAYERS -#define SF2_MAX_PLAYER_GROUP_NAME_LENGTH 32 - -static g_iPlayerGroupGlobalID = -1; -static g_iPlayerCurrentGroup[MAXPLAYERS + 1] = { -1, ... }; -static bool:g_bPlayerGroupActive[SF2_MAX_PLAYER_GROUPS] = { false, ... }; -static g_iPlayerGroupLeader[SF2_MAX_PLAYER_GROUPS] = { -1, ... }; -static g_iPlayerGroupID[SF2_MAX_PLAYER_GROUPS] = { -1, ... }; -static g_iPlayerGroupQueuePoints[SF2_MAX_PLAYER_GROUPS]; -static g_bPlayerGroupPlaying[SF2_MAX_PLAYER_GROUPS] = { false, ... }; -static Handle:g_hPlayerGroupNames; -static bool:g_bPlayerGroupInvitedPlayer[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; -static g_iPlayerGroupInvitedPlayerCount[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; -static Float:g_flPlayerGroupInvitedPlayerTime[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; - -SetupPlayerGroups() -{ - g_iPlayerGroupGlobalID = -1; - g_hPlayerGroupNames = CreateTrie(); -} - -stock GetPlayerGroupFromID(iGroupID) -{ - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - if (GetPlayerGroupID(i) == iGroupID) return i; - } - - return -1; -} - -SendPlayerGroupInvitation(client, iGroupID, iInviter=-1) -{ - if (!IsValidClient(client) || !IsClientParticipating(client)) - { - if (IsValidClient(iInviter)) - { - // TODO: Send message to the inviter that the client is invalid! - } - - return; - } - - if (!g_bPlayerEliminated[client]) - { - if (IsValidClient(iInviter)) - { - // TODO: Send message to the inviter that the client is currently in-game. - } - - return; - } - - new iGroupIndex = GetPlayerGroupFromID(iGroupID); - if (iGroupIndex == -1) return; - - new iMyGroupIndex = ClientGetPlayerGroup(client); - if (IsPlayerGroupActive(iMyGroupIndex)) - { - if (IsValidClient(iInviter)) - { - if (iMyGroupIndex == iGroupIndex) - { - CPrintToChat(iInviter, "%T", "SF2 Player In Group", iInviter); - } - else - { - CPrintToChat(iInviter, "%T", "SF2 Player In Another Group", iInviter); - } - } - - return; - } - - if (GetPlayerGroupMemberCount(iGroupIndex) >= GetMaxPlayersForRound()) - { - if (IsValidClient(iInviter)) - { - CPrintToChat(iInviter, "%T", "SF2 Group Is Full", iInviter); - } - - return; - } - - if (IsFakeClient(client)) - { - ClientSetPlayerGroup(client, iGroupIndex); - return; - } - - // Anti-spam. - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - - if (IsValidClient(iInviter)) - { - new Float:flNextInviteTime = GetPlayerGroupInvitedPlayerTime(iGroupIndex, client) + (20.0 * GetPlayerGroupInvitedPlayerCount(iGroupIndex, client)); - if (GetGameTime() < flNextInviteTime) - { - CPrintToChat(iInviter, "%T", "SF2 No Group Invite Spam", iInviter, RoundFloat(flNextInviteTime - GetGameTime()), sName); - return; - } - } - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - decl String:sLeaderName[64]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - - new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); - if (IsValidClient(iGroupLeader)) GetClientName(iGroupLeader, sLeaderName, sizeof(sLeaderName)); - else strcopy(sLeaderName, sizeof(sLeaderName), "nobody"); - - new Handle:hMenu = CreateMenu(Menu_GroupInvite); - SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Group Invite Menu Description", client, sLeaderName, sGroupName); - - decl String:sGroupID[64]; - IntToString(iGroupID, sGroupID, sizeof(sGroupID)); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, sGroupID, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - DisplayMenu(hMenu, client, 10); - - SetPlayerGroupInvitedPlayer(iGroupIndex, client, true); - SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, GetPlayerGroupInvitedPlayerCount(iGroupIndex, client) + 1); - SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, GetGameTime()); - - if (IsValidClient(iInviter)) - { - CPrintToChat(iInviter, "%T", "SF2 Group Invitation Sent", iInviter, sName); - } -} - -public Menu_GroupInvite(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) - { - CloseHandle(menu); - } - else if (action == MenuAction_Select) - { - if (param2 == 0) - { - decl String:sGroupID[64]; - GetMenuItem(menu, param2, sGroupID, sizeof(sGroupID)); - new iGroupIndex = GetPlayerGroupFromID(StringToInt(sGroupID)); - if (IsPlayerGroupActive(iGroupIndex)) - { - new iMyGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iMyGroupIndex)) - { - if (iMyGroupIndex == iGroupIndex) - { - CPrintToChat(param1, "%T", "SF2 In Group", param1); - } - else - { - CPrintToChat(param1, "%T", "SF2 In Another Group", param1); - } - } - else if (GetPlayerGroupMemberCount(iGroupIndex) >= GetMaxPlayersForRound()) - { - CPrintToChat(param1, "%T", "SF2 Group Is Full", param1); - } - else - { - ClientSetPlayerGroup(param1, iGroupIndex); - } - } - else - { - CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); - } - } - } -} - -DisplayResetGroupQueuePointsMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return; - } - - new Handle:hMenu = CreateMenu(Menu_ResetGroupQueuePoints); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Reset Group Queue Points Menu Title", client, "SF2 Reset Group Queue Points Menu Description", client); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, "0", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_ResetGroupQueuePoints(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) - { - if (param2 == 0) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) - { - SetPlayerGroupQueuePoints(iGroupIndex, 0); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - CPrintToChat(i, "%T", "SF2 Group Queue Points Reset", i); - } - } - } - else - { - CPrintToChat(param1, "%T", "SF2 Not Group Leader", param1); - } - } - - DisplayAdminGroupMenuToClient(param1); - } -} - -CheckPlayerGroup(iGroupIndex) -{ - if (!IsPlayerGroupActive(iGroupIndex)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START CheckPlayerGroup(%d)", iGroupIndex); -#endif - - new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); - if (iMemberCount <= 0) - { - RemovePlayerGroup(iGroupIndex); - } - else - { - // Remove any person that isn't participating. - for (new i = 1; i <= MaxClients; i++) - { - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - if (!IsValidClient(i) || !IsClientParticipating(i)) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("CheckPlayerGroup(%d): Invalid client detected (%d), removing from group", iGroupIndex, i); -#endif - - ClientSetPlayerGroup(i, -1); - } - } - } - - iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); - new iMaxPlayers = GetMaxPlayersForRound(); - new iExcessMemberCount = (iMemberCount - iMaxPlayers); - - if (iExcessMemberCount > 0) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("CheckPlayerGroup(%d): Excess members detected", iGroupIndex); -#endif - - new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); - if (IsValidClient(iGroupLeader)) - { - CPrintToChat(iGroupLeader, "%T", "SF2 Group Has Too Many Members", iGroupLeader); - } - - for (new i = 1, iCount; i <= MaxClients && iCount < iExcessMemberCount; i++) - { - if (!IsValidClient(i)) continue; - - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - if (i == iGroupLeader) continue; // Don't kick off the group leader. - - ClientSetPlayerGroup(i, -1); - iCount++; - } - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END CheckPlayerGroup(%d)", iGroupIndex); -#endif -} - -stock GetPlayerGroupCount() -{ - new iCount; - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (IsPlayerGroupActive(i)) iCount++; - } - - return iCount; -} - -stock CreatePlayerGroup() -{ - // Get an inactive group. - new iIndex = -1; - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) - { - iIndex = i; - break; - } - } - - if (iIndex != -1) - { - g_bPlayerGroupActive[iIndex] = true; - g_iPlayerGroupGlobalID++; - SetPlayerGroupID(iIndex, g_iPlayerGroupGlobalID); - ClearPlayerGroupMembers(iIndex); - SetPlayerGroupQueuePoints(iIndex, 0); - SetPlayerGroupLeader(iIndex, -1); - SetPlayerGroupName(iIndex, ""); - SetPlayerGroupPlaying(iIndex, false); - - for (new i = 1; i <= MaxClients; i++) - { - SetPlayerGroupInvitedPlayer(iIndex, i, false); - SetPlayerGroupInvitedPlayerCount(iIndex, i, 0); - SetPlayerGroupInvitedPlayerTime(iIndex, i, 0.0); - } - } - - return iIndex; -} - -stock RemovePlayerGroup(iGroupIndex) -{ - if (!IsPlayerGroupActive(iGroupIndex)) return; - - ClearPlayerGroupMembers(iGroupIndex); - SetPlayerGroupQueuePoints(iGroupIndex, 0); - SetPlayerGroupPlaying(iGroupIndex, false); - SetPlayerGroupLeader(iGroupIndex, -1); - g_bPlayerGroupActive[iGroupIndex] = false; - SetPlayerGroupID(iGroupIndex, -1); -} - -stock ClearPlayerGroupMembers(iGroupIndex) -{ - if (!IsPlayerGroupValid(iGroupIndex)) return; - - for (new i = 1; i <= MaxClients; i++) - { - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - ClientSetPlayerGroup(i, -1); - } - } -} - -stock bool:GetPlayerGroupName(iGroupIndex, String:sBuffer[], iBufferLen) -{ - decl String:sGroupIndex[32]; - IntToString(iGroupIndex, sGroupIndex, sizeof(sGroupIndex)); - return GetTrieString(g_hPlayerGroupNames, sGroupIndex, sBuffer, iBufferLen); -} - -stock SetPlayerGroupName(iGroupIndex, const String:sGroupName[]) -{ - decl String:sGroupIndex[32]; - IntToString(iGroupIndex, sGroupIndex, sizeof(sGroupIndex)); - SetTrieString(g_hPlayerGroupNames, sGroupIndex, sGroupName); -} - -stock GetPlayerGroupID(iGroupIndex) -{ - return g_iPlayerGroupID[iGroupIndex]; -} - -stock SetPlayerGroupID(iGroupIndex, iID) -{ - g_iPlayerGroupID[iGroupIndex] = iID; -} - -stock bool:IsPlayerGroupActive(iGroupIndex) -{ - return IsPlayerGroupValid(iGroupIndex) && g_bPlayerGroupActive[iGroupIndex]; -} - -stock bool:IsPlayerGroupValid(iGroupIndex) -{ - return (iGroupIndex >= 0 && iGroupIndex < SF2_MAX_PLAYER_GROUPS); -} - -stock GetPlayerGroupMemberCount(iGroupIndex) -{ - new iCount; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - iCount++; - } - } - - return iCount; -} - -stock bool:IsPlayerGroupPlaying(iGroupIndex) -{ - return (IsPlayerGroupActive(iGroupIndex) && g_bPlayerGroupPlaying[iGroupIndex]); -} - -stock SetPlayerGroupPlaying(iGroupIndex, bool:bToggle) -{ - g_bPlayerGroupPlaying[iGroupIndex] = bToggle; -} - -stock GetPlayerGroupLeader(iGroupIndex) -{ - return g_iPlayerGroupLeader[iGroupIndex]; -} - -stock SetPlayerGroupLeader(iGroupIndex, iGroupLeader) -{ - g_iPlayerGroupLeader[iGroupIndex] = iGroupLeader; - - if (IsValidClient(iGroupLeader)) - { - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - CPrintToChat(iGroupLeader, "%T", "SF2 New Group Leader", iGroupLeader, sGroupName); - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(iGroupLeader, sName, sizeof(sName)); - - for (new i = 1; i <= MaxClients; i++) - { - if (iGroupLeader == i || !IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - CPrintToChat(i, "%T", "SF2 Player New Group Leader", i, sName); - } - } - } -} - -PlayerGroupFindNewLeader(iGroupIndex) -{ - if (!IsPlayerGroupActive(iGroupIndex)) return -1; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - SetPlayerGroupLeader(iGroupIndex, i); - return i; - } - } - - return -1; -} - -stock GetPlayerGroupQueuePoints(iGroupIndex) -{ - return g_iPlayerGroupQueuePoints[iGroupIndex]; -} - -stock SetPlayerGroupQueuePoints(iGroupIndex, iAmount) -{ - g_iPlayerGroupQueuePoints[iGroupIndex] = iAmount; -} - -stock HasPlayerGroupInvitedPlayer(iGroupIndex, client) -{ - return g_bPlayerGroupInvitedPlayer[iGroupIndex][client]; -} - -stock SetPlayerGroupInvitedPlayer(iGroupIndex, client, bool:bToggle) -{ - g_bPlayerGroupInvitedPlayer[iGroupIndex][client] = bToggle; -} - -stock GetPlayerGroupInvitedPlayerCount(iGroupIndex, client) -{ - return g_iPlayerGroupInvitedPlayerCount[iGroupIndex][client]; -} - -stock SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, iAmount) -{ - g_iPlayerGroupInvitedPlayerCount[iGroupIndex][client] = iAmount; -} - -stock Float:GetPlayerGroupInvitedPlayerTime(iGroupIndex, client) -{ - return g_flPlayerGroupInvitedPlayerTime[iGroupIndex][client]; -} - -stock SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, Float:flTime) -{ - g_flPlayerGroupInvitedPlayerTime[iGroupIndex][client] = flTime; -} - -stock ClientGetPlayerGroup(client) -{ - return g_iPlayerCurrentGroup[client]; -} - -stock ClientSetPlayerGroup(client, iGroupIndex) -{ - new iOldPlayerGroup = ClientGetPlayerGroup(client); - if (iOldPlayerGroup == iGroupIndex) return; // No change. - - g_iPlayerCurrentGroup[client] = iGroupIndex; - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - - if (IsPlayerGroupActive(iOldPlayerGroup)) - { - SetPlayerGroupInvitedPlayer(iOldPlayerGroup, client, false); - SetPlayerGroupInvitedPlayerCount(iOldPlayerGroup, client, 0); - SetPlayerGroupInvitedPlayerTime(iOldPlayerGroup, client, 0.0); - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iOldPlayerGroup, sGroupName, sizeof(sGroupName)); - CPrintToChat(client, "%T", "SF2 Left Group", client, sGroupName); - - for (new i = 1; i <= MaxClients; i++) - { - if (i == client || !IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) == iOldPlayerGroup) - { - CPrintToChat(i, "%T", "SF2 Player Left Group", i, sName); - } - } - - new iOldGroupLeader = GetPlayerGroupLeader(iOldPlayerGroup); - if (iOldGroupLeader == client) - { - new iOldGroupNewLeader = PlayerGroupFindNewLeader(iOldPlayerGroup); - if (iOldGroupNewLeader == -1) - { - // Couldn't find a new leader. This group has no leader! - SetPlayerGroupLeader(iOldPlayerGroup, -1); - } - } - - CheckPlayerGroup(iOldPlayerGroup); - } - - if (IsPlayerGroupPlaying(iGroupIndex)) - { - ClientSetQueuePoints(client, 0); - } - - if (IsPlayerGroupActive(iGroupIndex)) - { - SetPlayerGroupInvitedPlayer(iGroupIndex, client, false); - SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, 0); - SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, 0.0); - - // Set the player's personal queue points to 0. - //ClientSetQueuePoints(client, 0); - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - CPrintToChat(client, "%T", "SF2 Joined Group", client, sGroupName); - - for (new i = 1; i <= MaxClients; i++) - { - if (i == client || !IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) == iGroupIndex) - { - CPrintToChat(i, "%T", "SF2 Player Joined Group", i, sName); - } - } - } +#if defined _sf2_playergroups_included + #endinput +#endif +#define _sf2_playergroups_included + +#define SF2_MAX_PLAYER_GROUPS MAXPLAYERS +#define SF2_MAX_PLAYER_GROUP_NAME_LENGTH 32 + +static g_iPlayerGroupGlobalID = -1; +static g_iPlayerCurrentGroup[MAXPLAYERS + 1] = { -1, ... }; +static bool:g_bPlayerGroupActive[SF2_MAX_PLAYER_GROUPS] = { false, ... }; +static g_iPlayerGroupLeader[SF2_MAX_PLAYER_GROUPS] = { -1, ... }; +static g_iPlayerGroupID[SF2_MAX_PLAYER_GROUPS] = { -1, ... }; +static g_iPlayerGroupQueuePoints[SF2_MAX_PLAYER_GROUPS]; +static g_bPlayerGroupPlaying[SF2_MAX_PLAYER_GROUPS] = { false, ... }; +static Handle:g_hPlayerGroupNames; +static bool:g_bPlayerGroupInvitedPlayer[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; +static g_iPlayerGroupInvitedPlayerCount[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; +static Float:g_flPlayerGroupInvitedPlayerTime[SF2_MAX_PLAYER_GROUPS][MAXPLAYERS + 1]; + +SetupPlayerGroups() +{ + g_iPlayerGroupGlobalID = -1; + g_hPlayerGroupNames = CreateTrie(); +} + +stock GetPlayerGroupFromID(iGroupID) +{ + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + if (GetPlayerGroupID(i) == iGroupID) return i; + } + + return -1; +} + +SendPlayerGroupInvitation(client, iGroupID, iInviter=-1) +{ + if (!IsValidClient(client) || !IsClientParticipating(client)) + { + if (IsValidClient(iInviter)) + { + // TODO: Send message to the inviter that the client is invalid! + } + + return; + } + + if (!g_bPlayerEliminated[client]) + { + if (IsValidClient(iInviter)) + { + // TODO: Send message to the inviter that the client is currently in-game. + } + + return; + } + + new iGroupIndex = GetPlayerGroupFromID(iGroupID); + if (iGroupIndex == -1) return; + + new iMyGroupIndex = ClientGetPlayerGroup(client); + if (IsPlayerGroupActive(iMyGroupIndex)) + { + if (IsValidClient(iInviter)) + { + if (iMyGroupIndex == iGroupIndex) + { + CPrintToChat(iInviter, "%T", "SF2 Player In Group", iInviter); + } + else + { + CPrintToChat(iInviter, "%T", "SF2 Player In Another Group", iInviter); + } + } + + return; + } + + if (GetPlayerGroupMemberCount(iGroupIndex) >= GetMaxPlayersForRound()) + { + if (IsValidClient(iInviter)) + { + CPrintToChat(iInviter, "%T", "SF2 Group Is Full", iInviter); + } + + return; + } + + if (IsFakeClient(client)) + { + ClientSetPlayerGroup(client, iGroupIndex); + return; + } + + // Anti-spam. + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + if (IsValidClient(iInviter)) + { + new Float:flNextInviteTime = GetPlayerGroupInvitedPlayerTime(iGroupIndex, client) + (20.0 * GetPlayerGroupInvitedPlayerCount(iGroupIndex, client)); + if (GetGameTime() < flNextInviteTime) + { + CPrintToChat(iInviter, "%T", "SF2 No Group Invite Spam", iInviter, RoundFloat(flNextInviteTime - GetGameTime()), sName); + return; + } + } + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + decl String:sLeaderName[64]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + + new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); + if (IsValidClient(iGroupLeader)) GetClientName(iGroupLeader, sLeaderName, sizeof(sLeaderName)); + else strcopy(sLeaderName, sizeof(sLeaderName), "nobody"); + + new Handle:hMenu = CreateMenu(Menu_GroupInvite); + SetMenuTitle(hMenu, "%t%T\n \n", "SF2 Prefix", "SF2 Group Invite Menu Description", client, sLeaderName, sGroupName); + + decl String:sGroupID[64]; + IntToString(iGroupID, sGroupID, sizeof(sGroupID)); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, sGroupID, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + DisplayMenu(hMenu, client, 10); + + SetPlayerGroupInvitedPlayer(iGroupIndex, client, true); + SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, GetPlayerGroupInvitedPlayerCount(iGroupIndex, client) + 1); + SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, GetGameTime()); + + if (IsValidClient(iInviter)) + { + CPrintToChat(iInviter, "%T", "SF2 Group Invitation Sent", iInviter, sName); + } +} + +public Menu_GroupInvite(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) + { + CloseHandle(menu); + } + else if (action == MenuAction_Select) + { + if (param2 == 0) + { + decl String:sGroupID[64]; + GetMenuItem(menu, param2, sGroupID, sizeof(sGroupID)); + new iGroupIndex = GetPlayerGroupFromID(StringToInt(sGroupID)); + if (IsPlayerGroupActive(iGroupIndex)) + { + new iMyGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iMyGroupIndex)) + { + if (iMyGroupIndex == iGroupIndex) + { + CPrintToChat(param1, "%T", "SF2 In Group", param1); + } + else + { + CPrintToChat(param1, "%T", "SF2 In Another Group", param1); + } + } + else if (GetPlayerGroupMemberCount(iGroupIndex) >= GetMaxPlayersForRound()) + { + CPrintToChat(param1, "%T", "SF2 Group Is Full", param1); + } + else + { + ClientSetPlayerGroup(param1, iGroupIndex); + } + } + else + { + CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); + } + } + } +} + +DisplayResetGroupQueuePointsMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return; + } + + new Handle:hMenu = CreateMenu(Menu_ResetGroupQueuePoints); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Reset Group Queue Points Menu Title", client, "SF2 Reset Group Queue Points Menu Description", client); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, "0", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_ResetGroupQueuePoints(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) + { + if (param2 == 0) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) + { + SetPlayerGroupQueuePoints(iGroupIndex, 0); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + CPrintToChat(i, "%T", "SF2 Group Queue Points Reset", i); + } + } + } + else + { + CPrintToChat(param1, "%T", "SF2 Not Group Leader", param1); + } + } + + DisplayAdminGroupMenuToClient(param1); + } +} + +CheckPlayerGroup(iGroupIndex) +{ + if (!IsPlayerGroupActive(iGroupIndex)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START CheckPlayerGroup(%d)", iGroupIndex); +#endif + + new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); + if (iMemberCount <= 0) + { + RemovePlayerGroup(iGroupIndex); + } + else + { + // Remove any person that isn't participating. + for (new i = 1; i <= MaxClients; i++) + { + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + if (!IsValidClient(i) || !IsClientParticipating(i)) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("CheckPlayerGroup(%d): Invalid client detected (%d), removing from group", iGroupIndex, i); +#endif + + ClientSetPlayerGroup(i, -1); + } + } + } + + iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); + new iMaxPlayers = GetMaxPlayersForRound(); + new iExcessMemberCount = (iMemberCount - iMaxPlayers); + + if (iExcessMemberCount > 0) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("CheckPlayerGroup(%d): Excess members detected", iGroupIndex); +#endif + + new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); + if (IsValidClient(iGroupLeader)) + { + CPrintToChat(iGroupLeader, "%T", "SF2 Group Has Too Many Members", iGroupLeader); + } + + for (new i = 1, iCount; i <= MaxClients && iCount < iExcessMemberCount; i++) + { + if (!IsValidClient(i)) continue; + + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + if (i == iGroupLeader) continue; // Don't kick off the group leader. + + ClientSetPlayerGroup(i, -1); + iCount++; + } + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END CheckPlayerGroup(%d)", iGroupIndex); +#endif +} + +stock GetPlayerGroupCount() +{ + new iCount; + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (IsPlayerGroupActive(i)) iCount++; + } + + return iCount; +} + +stock CreatePlayerGroup() +{ + // Get an inactive group. + new iIndex = -1; + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) + { + iIndex = i; + break; + } + } + + if (iIndex != -1) + { + g_bPlayerGroupActive[iIndex] = true; + g_iPlayerGroupGlobalID++; + SetPlayerGroupID(iIndex, g_iPlayerGroupGlobalID); + ClearPlayerGroupMembers(iIndex); + SetPlayerGroupQueuePoints(iIndex, 0); + SetPlayerGroupLeader(iIndex, -1); + SetPlayerGroupName(iIndex, ""); + SetPlayerGroupPlaying(iIndex, false); + + for (new i = 1; i <= MaxClients; i++) + { + SetPlayerGroupInvitedPlayer(iIndex, i, false); + SetPlayerGroupInvitedPlayerCount(iIndex, i, 0); + SetPlayerGroupInvitedPlayerTime(iIndex, i, 0.0); + } + } + + return iIndex; +} + +stock RemovePlayerGroup(iGroupIndex) +{ + if (!IsPlayerGroupActive(iGroupIndex)) return; + + ClearPlayerGroupMembers(iGroupIndex); + SetPlayerGroupQueuePoints(iGroupIndex, 0); + SetPlayerGroupPlaying(iGroupIndex, false); + SetPlayerGroupLeader(iGroupIndex, -1); + g_bPlayerGroupActive[iGroupIndex] = false; + SetPlayerGroupID(iGroupIndex, -1); +} + +stock ClearPlayerGroupMembers(iGroupIndex) +{ + if (!IsPlayerGroupValid(iGroupIndex)) return; + + for (new i = 1; i <= MaxClients; i++) + { + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + ClientSetPlayerGroup(i, -1); + } + } +} + +stock bool:GetPlayerGroupName(iGroupIndex, String:sBuffer[], iBufferLen) +{ + decl String:sGroupIndex[32]; + IntToString(iGroupIndex, sGroupIndex, sizeof(sGroupIndex)); + return GetTrieString(g_hPlayerGroupNames, sGroupIndex, sBuffer, iBufferLen); +} + +stock SetPlayerGroupName(iGroupIndex, const String:sGroupName[]) +{ + decl String:sGroupIndex[32]; + IntToString(iGroupIndex, sGroupIndex, sizeof(sGroupIndex)); + SetTrieString(g_hPlayerGroupNames, sGroupIndex, sGroupName); +} + +stock GetPlayerGroupID(iGroupIndex) +{ + return g_iPlayerGroupID[iGroupIndex]; +} + +stock SetPlayerGroupID(iGroupIndex, iID) +{ + g_iPlayerGroupID[iGroupIndex] = iID; +} + +stock bool:IsPlayerGroupActive(iGroupIndex) +{ + return IsPlayerGroupValid(iGroupIndex) && g_bPlayerGroupActive[iGroupIndex]; +} + +stock bool:IsPlayerGroupValid(iGroupIndex) +{ + return (iGroupIndex >= 0 && iGroupIndex < SF2_MAX_PLAYER_GROUPS); +} + +stock GetPlayerGroupMemberCount(iGroupIndex) +{ + new iCount; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + iCount++; + } + } + + return iCount; +} + +stock bool:IsPlayerGroupPlaying(iGroupIndex) +{ + return (IsPlayerGroupActive(iGroupIndex) && g_bPlayerGroupPlaying[iGroupIndex]); +} + +stock SetPlayerGroupPlaying(iGroupIndex, bool:bToggle) +{ + g_bPlayerGroupPlaying[iGroupIndex] = bToggle; +} + +stock GetPlayerGroupLeader(iGroupIndex) +{ + return g_iPlayerGroupLeader[iGroupIndex]; +} + +stock SetPlayerGroupLeader(iGroupIndex, iGroupLeader) +{ + g_iPlayerGroupLeader[iGroupIndex] = iGroupLeader; + + if (IsValidClient(iGroupLeader)) + { + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + CPrintToChat(iGroupLeader, "%T", "SF2 New Group Leader", iGroupLeader, sGroupName); + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(iGroupLeader, sName, sizeof(sName)); + + for (new i = 1; i <= MaxClients; i++) + { + if (iGroupLeader == i || !IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + CPrintToChat(i, "%T", "SF2 Player New Group Leader", i, sName); + } + } + } +} + +PlayerGroupFindNewLeader(iGroupIndex) +{ + if (!IsPlayerGroupActive(iGroupIndex)) return -1; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + SetPlayerGroupLeader(iGroupIndex, i); + return i; + } + } + + return -1; +} + +stock GetPlayerGroupQueuePoints(iGroupIndex) +{ + return g_iPlayerGroupQueuePoints[iGroupIndex]; +} + +stock SetPlayerGroupQueuePoints(iGroupIndex, iAmount) +{ + g_iPlayerGroupQueuePoints[iGroupIndex] = iAmount; +} + +stock HasPlayerGroupInvitedPlayer(iGroupIndex, client) +{ + return g_bPlayerGroupInvitedPlayer[iGroupIndex][client]; +} + +stock SetPlayerGroupInvitedPlayer(iGroupIndex, client, bool:bToggle) +{ + g_bPlayerGroupInvitedPlayer[iGroupIndex][client] = bToggle; +} + +stock GetPlayerGroupInvitedPlayerCount(iGroupIndex, client) +{ + return g_iPlayerGroupInvitedPlayerCount[iGroupIndex][client]; +} + +stock SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, iAmount) +{ + g_iPlayerGroupInvitedPlayerCount[iGroupIndex][client] = iAmount; +} + +stock Float:GetPlayerGroupInvitedPlayerTime(iGroupIndex, client) +{ + return g_flPlayerGroupInvitedPlayerTime[iGroupIndex][client]; +} + +stock SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, Float:flTime) +{ + g_flPlayerGroupInvitedPlayerTime[iGroupIndex][client] = flTime; +} + +stock ClientGetPlayerGroup(client) +{ + return g_iPlayerCurrentGroup[client]; +} + +stock ClientSetPlayerGroup(client, iGroupIndex) +{ + new iOldPlayerGroup = ClientGetPlayerGroup(client); + if (iOldPlayerGroup == iGroupIndex) return; // No change. + + g_iPlayerCurrentGroup[client] = iGroupIndex; + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + if (IsPlayerGroupActive(iOldPlayerGroup)) + { + SetPlayerGroupInvitedPlayer(iOldPlayerGroup, client, false); + SetPlayerGroupInvitedPlayerCount(iOldPlayerGroup, client, 0); + SetPlayerGroupInvitedPlayerTime(iOldPlayerGroup, client, 0.0); + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iOldPlayerGroup, sGroupName, sizeof(sGroupName)); + CPrintToChat(client, "%T", "SF2 Left Group", client, sGroupName); + + for (new i = 1; i <= MaxClients; i++) + { + if (i == client || !IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) == iOldPlayerGroup) + { + CPrintToChat(i, "%T", "SF2 Player Left Group", i, sName); + } + } + + new iOldGroupLeader = GetPlayerGroupLeader(iOldPlayerGroup); + if (iOldGroupLeader == client) + { + new iOldGroupNewLeader = PlayerGroupFindNewLeader(iOldPlayerGroup); + if (iOldGroupNewLeader == -1) + { + // Couldn't find a new leader. This group has no leader! + SetPlayerGroupLeader(iOldPlayerGroup, -1); + } + } + + CheckPlayerGroup(iOldPlayerGroup); + } + + if (IsPlayerGroupPlaying(iGroupIndex)) + { + ClientSetQueuePoints(client, 0); + } + + if (IsPlayerGroupActive(iGroupIndex)) + { + SetPlayerGroupInvitedPlayer(iGroupIndex, client, false); + SetPlayerGroupInvitedPlayerCount(iGroupIndex, client, 0); + SetPlayerGroupInvitedPlayerTime(iGroupIndex, client, 0.0); + + // Set the player's personal queue points to 0. + //ClientSetQueuePoints(client, 0); + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + CPrintToChat(client, "%T", "SF2 Joined Group", client, sGroupName); + + for (new i = 1; i <= MaxClients; i++) + { + if (i == client || !IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) == iGroupIndex) + { + CPrintToChat(i, "%T", "SF2 Player Joined Group", i, sName); + } + } + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/playergroups/menus.sp b/addons/sourcemod/scripting/rytp_horror/playergroups/menus.sp index 9cdecb7..633b512 100644 --- a/addons/sourcemod/scripting/rytp_horror/playergroups/menus.sp +++ b/addons/sourcemod/scripting/rytp_horror/playergroups/menus.sp @@ -1,620 +1,620 @@ -#if defined _sf2_playergroups_menus - #endinput -#endif - -#define _sf2_playergroups_menus - -DisplayGroupMainMenuToClient(client) -{ - new Handle:hMenu = CreateMenu(Menu_GroupMain); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Group Main Menu Title", client, "SF2 Group Main Menu Description", client); - - new iGroupIndex = ClientGetPlayerGroup(client); - new bool:bGroupIsActive = IsPlayerGroupActive(iGroupIndex); - - decl String:sBuffer[256]; - if (bGroupIsActive && GetPlayerGroupLeader(iGroupIndex) == client) - { - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Group Menu Title", client); - } - else - { - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 View Current Group Info Menu Title", client); - } - - AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Create Group Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Leave Group Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_GroupMain(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayMenu(g_hMenuMain, param1, 30); - } - else if (action == MenuAction_Select) - { - switch (param2) - { - case 0: DisplayAdminGroupMenuToClient(param1); - case 1: DisplayCreateGroupMenuToClient(param1); - case 2: DisplayLeaveGroupMenuToClient(param1); - } - } -} - -DisplayCreateGroupMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (IsPlayerGroupActive(iGroupIndex)) - { - // He's already in a group. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 In Group", client); - return; - } - - new Handle:hMenu = CreateMenu(Menu_CreateGroup); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Create Group Menu Title", client, "SF2 Create Group Menu Description", client, GetConVarInt(g_cvMaxPlayers), g_iPlayerQueuePoints[client]); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, "0", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_CreateGroup(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Select) - { - if (param2 == 0) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex)) - { - CPrintToChat(param1, "%T", "SF2 In Group", param1); - } - else - { - iGroupIndex = CreatePlayerGroup(); - if (iGroupIndex != -1) - { - new iQueuePoints = g_iPlayerQueuePoints[param1]; - - decl String:sGroupName[64]; - Format(sGroupName, sizeof(sGroupName), "Group %d", iGroupIndex); - SetPlayerGroupName(iGroupIndex, sGroupName); - ClientSetPlayerGroup(param1, iGroupIndex); - SetPlayerGroupLeader(iGroupIndex, param1); - SetPlayerGroupQueuePoints(iGroupIndex, iQueuePoints); - - CPrintToChat(param1, "%T", "SF2 Created Group", param1, sGroupName); - } - else - { - CPrintToChat(param1, "%T", "SF2 Max Groups Reached", param1); - } - } - } - - DisplayGroupMainMenuToClient(param1); - } -} - -DisplayLeaveGroupMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - - new Handle:hMenu = CreateMenu(Menu_LeaveGroup); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Leave Group Menu Title", client, "SF2 Leave Group Menu Description", client, sGroupName); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, "0", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_LeaveGroup(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Select) - { - if (param2 == 0) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (!IsPlayerGroupActive(iGroupIndex)) - { - CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); - } - else - { - ClientSetPlayerGroup(param1, -1); - } - } - - DisplayGroupMainMenuToClient(param1); - } -} - -DisplayAdminGroupMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - - decl String:sLeaderName[MAX_NAME_LENGTH]; - new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); - if (IsValidClient(iGroupLeader)) GetClientName(iGroupLeader, sLeaderName, sizeof(sLeaderName)); - else strcopy(sLeaderName, sizeof(sLeaderName), "---"); - - new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); - new iMaxPlayers = GetConVarInt(g_cvMaxPlayers); - new iQueuePoints = GetPlayerGroupQueuePoints(iGroupIndex); - - new Handle:hMenu = CreateMenu(Menu_AdminGroup); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Admin Group Menu Title", client, "SF2 Admin Group Menu Description", client, sGroupName, sLeaderName, iMemberCount, iMaxPlayers, iQueuePoints); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 View Group Members Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Set Group Name Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Set Group Leader Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Invite To Group Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client && iMemberCount < iMaxPlayers ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Kick From Group Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client && iMemberCount > 1 ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Reset Group Queue Points Menu Title", client); - AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_AdminGroup(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayGroupMainMenuToClient(param1); - } - else if (action == MenuAction_Select) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex)) - { - switch (param2) - { - case 0: DisplayViewGroupMembersMenuToClient(param1); - case 1: DisplaySetGroupNameMenuToClient(param1); - case 2: DisplaySetGroupLeaderMenuToClient(param1); - case 3: DisplayInviteToGroupMenuToClient(param1); - case 4: DisplayKickFromGroupMenuToClient(param1); - case 5: DisplayResetGroupQueuePointsMenuToClient(param1); - } - } - else - { - DisplayGroupMainMenuToClient(param1); - CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); - } - } -} - -DisplayViewGroupMembersMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - new Handle:hPlayers = CreateArray(); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - new iTempGroup = ClientGetPlayerGroup(i); - if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; - - PushArrayCell(hPlayers, i); - } - - new iPlayerCount = GetArraySize(hPlayers); - if (iPlayerCount) - { - new Handle:hMenu = CreateMenu(Menu_ViewGroupMembers); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 View Group Members Menu Title", client, "SF2 View Group Members Menu Description", client); - - decl String:sUserId[32]; - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < iPlayerCount; i++) - { - new iClient = GetArrayCell(hPlayers, i); - IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); - GetClientName(iClient, sName, sizeof(sName)); - AddMenuItem(hMenu, sUserId, sName); - } - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - } - else - { - // No players left for the taking! - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 No Players Available", client); - } - - CloseHandle(hPlayers); -} - -public Menu_ViewGroupMembers(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) DisplayAdminGroupMenuToClient(param1); -} - -DisplaySetGroupLeaderMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return; - } - - new Handle:hPlayers = CreateArray(); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - new iTempGroup = ClientGetPlayerGroup(i); - if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; - if (i == client) continue; - - PushArrayCell(hPlayers, i); - } - - new iPlayerCount = GetArraySize(hPlayers); - if (iPlayerCount) - { - new Handle:hMenu = CreateMenu(Menu_SetGroupLeader); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Set Group Leader Menu Title", client, "SF2 Set Group Leader Menu Description", client); - - decl String:sUserId[32]; - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < iPlayerCount; i++) - { - new iClient = GetArrayCell(hPlayers, i); - IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); - GetClientName(iClient, sName, sizeof(sName)); - AddMenuItem(hMenu, sUserId, sName); - } - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - } - else - { - // No players left for the taking! - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 No Players Available", client); - } - - CloseHandle(hPlayers); -} - -public Menu_SetGroupLeader(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) - { - decl String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - new userid = StringToInt(sInfo); - new iPlayer = GetClientOfUserId(userid); - - if (ClientGetPlayerGroup(iPlayer) == iGroupIndex && IsValidClient(iPlayer)) - { - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - decl String:sName[MAX_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - GetClientName(iPlayer, sName, sizeof(sName)); - - SetPlayerGroupLeader(iGroupIndex, iPlayer); - } - else - { - CPrintToChat(param1, "%T", "SF2 Player Not In Group", param1); - } - } - - DisplayAdminGroupMenuToClient(param1); - } -} - -DisplayKickFromGroupMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return; - } - - new Handle:hPlayers = CreateArray(); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - - new iTempGroup = ClientGetPlayerGroup(i); - if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; - if (i == client) continue; - - PushArrayCell(hPlayers, i); - } - - new iPlayerCount = GetArraySize(hPlayers); - if (iPlayerCount) - { - new Handle:hMenu = CreateMenu(Menu_KickFromGroup); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Kick From Group Menu Title", client, "SF2 Kick From Group Menu Description", client); - - decl String:sUserId[32]; - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < iPlayerCount; i++) - { - new iClient = GetArrayCell(hPlayers, i); - IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); - GetClientName(iClient, sName, sizeof(sName)); - AddMenuItem(hMenu, sUserId, sName); - } - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - } - else - { - // No players left for the taking! - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 No Players Available", client); - } - - CloseHandle(hPlayers); -} - -public Menu_KickFromGroup(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) - { - decl String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - new userid = StringToInt(sInfo); - new iPlayer = GetClientOfUserId(userid); - - if (ClientGetPlayerGroup(iPlayer) == iGroupIndex) - { - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - decl String:sName[MAX_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); - GetClientName(iPlayer, sName, sizeof(sName)); - - CPrintToChat(iPlayer, "%T", "SF2 Kicked From Group", iPlayer, sGroupName); - ClientSetPlayerGroup(iPlayer, -1); - CPrintToChat(param1, "%T", "SF2 Player Kicked From Group", param1, sName); - } - else - { - CPrintToChat(param1, "%T", "SF2 Player Not In Group", param1); - } - } - - DisplayKickFromGroupMenuToClient(param1); - } -} - -DisplaySetGroupNameMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return; - } - - new Handle:hMenu = CreateMenu(Menu_SetGroupName); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Set Group Name Menu Title", client, "SF2 Set Group Name Menu Description", client); - - decl String:sBuffer[256]; - Format(sBuffer, sizeof(sBuffer), "%T", "Back", client); - AddMenuItem(hMenu, "0", sBuffer); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); -} - -public Menu_SetGroupName(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) DisplayAdminGroupMenuToClient(param1); -} - -DisplayInviteToGroupMenuToClient(client) -{ - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - // His group isn't valid anymore. Take him back to the main menu. - DisplayGroupMainMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return; - } - - if (GetPlayerGroupMemberCount(iGroupIndex) >= GetConVarInt(g_cvMaxPlayers)) - { - // His group is full! - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 Group Is Full", client); - return; - } - - new Handle:hPlayers = CreateArray(); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i) || !IsClientParticipating(i)) continue; - - new iTempGroup = ClientGetPlayerGroup(i); - if (IsPlayerGroupActive(iTempGroup)) continue; - if (!g_bPlayerEliminated[i]) continue; - - PushArrayCell(hPlayers, i); - } - - new iPlayerCount = GetArraySize(hPlayers); - if (iPlayerCount) - { - new Handle:hMenu = CreateMenu(Menu_InviteToGroup); - SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Invite To Group Menu Title", client, "SF2 Invite To Group Menu Description", client); - - decl String:sUserId[32]; - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < iPlayerCount; i++) - { - new iClient = GetArrayCell(hPlayers, i); - IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); - GetClientName(iClient, sName, sizeof(sName)); - AddMenuItem(hMenu, sUserId, sName); - } - - SetMenuExitBackButton(hMenu, true); - DisplayMenu(hMenu, client, MENU_TIME_FOREVER); - } - else - { - // No players left for the taking! - DisplayAdminGroupMenuToClient(client); - CPrintToChat(client, "%T", "SF2 No Players Available", client); - } - - CloseHandle(hPlayers); -} - -public Menu_InviteToGroup(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_End) CloseHandle(menu); - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); - } - else if (action == MenuAction_Select) - { - new iGroupIndex = ClientGetPlayerGroup(param1); - if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) - { - decl String:sInfo[64]; - GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); - new userid = StringToInt(sInfo); - new iInvitedPlayer = GetClientOfUserId(userid); - SendPlayerGroupInvitation(iInvitedPlayer, GetPlayerGroupID(iGroupIndex), param1); - } - - DisplayInviteToGroupMenuToClient(param1); - } +#if defined _sf2_playergroups_menus + #endinput +#endif + +#define _sf2_playergroups_menus + +DisplayGroupMainMenuToClient(client) +{ + new Handle:hMenu = CreateMenu(Menu_GroupMain); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Group Main Menu Title", client, "SF2 Group Main Menu Description", client); + + new iGroupIndex = ClientGetPlayerGroup(client); + new bool:bGroupIsActive = IsPlayerGroupActive(iGroupIndex); + + decl String:sBuffer[256]; + if (bGroupIsActive && GetPlayerGroupLeader(iGroupIndex) == client) + { + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Admin Group Menu Title", client); + } + else + { + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 View Current Group Info Menu Title", client); + } + + AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Create Group Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Leave Group Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, bGroupIsActive ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_GroupMain(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayMenu(g_hMenuMain, param1, 30); + } + else if (action == MenuAction_Select) + { + switch (param2) + { + case 0: DisplayAdminGroupMenuToClient(param1); + case 1: DisplayCreateGroupMenuToClient(param1); + case 2: DisplayLeaveGroupMenuToClient(param1); + } + } +} + +DisplayCreateGroupMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (IsPlayerGroupActive(iGroupIndex)) + { + // He's already in a group. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 In Group", client); + return; + } + + new Handle:hMenu = CreateMenu(Menu_CreateGroup); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Create Group Menu Title", client, "SF2 Create Group Menu Description", client, GetMaxPlayersForRound(), g_iPlayerQueuePoints[client]); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, "0", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_CreateGroup(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Select) + { + if (param2 == 0) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex)) + { + CPrintToChat(param1, "%T", "SF2 In Group", param1); + } + else + { + iGroupIndex = CreatePlayerGroup(); + if (iGroupIndex != -1) + { + new iQueuePoints = g_iPlayerQueuePoints[param1]; + + decl String:sGroupName[64]; + Format(sGroupName, sizeof(sGroupName), "Group %d", iGroupIndex); + SetPlayerGroupName(iGroupIndex, sGroupName); + ClientSetPlayerGroup(param1, iGroupIndex); + SetPlayerGroupLeader(iGroupIndex, param1); + SetPlayerGroupQueuePoints(iGroupIndex, iQueuePoints); + + CPrintToChat(param1, "%T", "SF2 Created Group", param1, sGroupName); + } + else + { + CPrintToChat(param1, "%T", "SF2 Max Groups Reached", param1); + } + } + } + + DisplayGroupMainMenuToClient(param1); + } +} + +DisplayLeaveGroupMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + + new Handle:hMenu = CreateMenu(Menu_LeaveGroup); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Leave Group Menu Title", client, "SF2 Leave Group Menu Description", client, sGroupName); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, "0", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_LeaveGroup(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Select) + { + if (param2 == 0) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (!IsPlayerGroupActive(iGroupIndex)) + { + CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); + } + else + { + ClientSetPlayerGroup(param1, -1); + } + } + + DisplayGroupMainMenuToClient(param1); + } +} + +DisplayAdminGroupMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + + decl String:sLeaderName[MAX_NAME_LENGTH]; + new iGroupLeader = GetPlayerGroupLeader(iGroupIndex); + if (IsValidClient(iGroupLeader)) GetClientName(iGroupLeader, sLeaderName, sizeof(sLeaderName)); + else strcopy(sLeaderName, sizeof(sLeaderName), "---"); + + new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); + new iMaxPlayers = GetMaxPlayersForRound(); + new iQueuePoints = GetPlayerGroupQueuePoints(iGroupIndex); + + new Handle:hMenu = CreateMenu(Menu_AdminGroup); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Admin Group Menu Title", client, "SF2 Admin Group Menu Description", client, sGroupName, sLeaderName, iMemberCount, iMaxPlayers, iQueuePoints); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 View Group Members Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Set Group Name Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Set Group Leader Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Invite To Group Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client && iMemberCount < iMaxPlayers ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Kick From Group Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client && iMemberCount > 1 ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + Format(sBuffer, sizeof(sBuffer), "%T", "SF2 Reset Group Queue Points Menu Title", client); + AddMenuItem(hMenu, "0", sBuffer, iGroupLeader == client ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED); + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_AdminGroup(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayGroupMainMenuToClient(param1); + } + else if (action == MenuAction_Select) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex)) + { + switch (param2) + { + case 0: DisplayViewGroupMembersMenuToClient(param1); + case 1: DisplaySetGroupNameMenuToClient(param1); + case 2: DisplaySetGroupLeaderMenuToClient(param1); + case 3: DisplayInviteToGroupMenuToClient(param1); + case 4: DisplayKickFromGroupMenuToClient(param1); + case 5: DisplayResetGroupQueuePointsMenuToClient(param1); + } + } + else + { + DisplayGroupMainMenuToClient(param1); + CPrintToChat(param1, "%T", "SF2 Group Does Not Exist", param1); + } + } +} + +DisplayViewGroupMembersMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + new Handle:hPlayers = CreateArray(); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + new iTempGroup = ClientGetPlayerGroup(i); + if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; + + PushArrayCell(hPlayers, i); + } + + new iPlayerCount = GetArraySize(hPlayers); + if (iPlayerCount) + { + new Handle:hMenu = CreateMenu(Menu_ViewGroupMembers); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 View Group Members Menu Title", client, "SF2 View Group Members Menu Description", client); + + decl String:sUserId[32]; + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < iPlayerCount; i++) + { + new iClient = GetArrayCell(hPlayers, i); + IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); + GetClientName(iClient, sName, sizeof(sName)); + AddMenuItem(hMenu, sUserId, sName); + } + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + } + else + { + // No players left for the taking! + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 No Players Available", client); + } + + CloseHandle(hPlayers); +} + +public Menu_ViewGroupMembers(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) DisplayAdminGroupMenuToClient(param1); +} + +DisplaySetGroupLeaderMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return; + } + + new Handle:hPlayers = CreateArray(); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + new iTempGroup = ClientGetPlayerGroup(i); + if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; + if (i == client) continue; + + PushArrayCell(hPlayers, i); + } + + new iPlayerCount = GetArraySize(hPlayers); + if (iPlayerCount) + { + new Handle:hMenu = CreateMenu(Menu_SetGroupLeader); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Set Group Leader Menu Title", client, "SF2 Set Group Leader Menu Description", client); + + decl String:sUserId[32]; + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < iPlayerCount; i++) + { + new iClient = GetArrayCell(hPlayers, i); + IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); + GetClientName(iClient, sName, sizeof(sName)); + AddMenuItem(hMenu, sUserId, sName); + } + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + } + else + { + // No players left for the taking! + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 No Players Available", client); + } + + CloseHandle(hPlayers); +} + +public Menu_SetGroupLeader(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) + { + decl String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + new userid = StringToInt(sInfo); + new iPlayer = GetClientOfUserId(userid); + + if (ClientGetPlayerGroup(iPlayer) == iGroupIndex && IsValidClient(iPlayer)) + { + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + decl String:sName[MAX_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + GetClientName(iPlayer, sName, sizeof(sName)); + + SetPlayerGroupLeader(iGroupIndex, iPlayer); + } + else + { + CPrintToChat(param1, "%T", "SF2 Player Not In Group", param1); + } + } + + DisplayAdminGroupMenuToClient(param1); + } +} + +DisplayKickFromGroupMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return; + } + + new Handle:hPlayers = CreateArray(); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + + new iTempGroup = ClientGetPlayerGroup(i); + if (!IsPlayerGroupActive(iTempGroup) || iTempGroup != iGroupIndex) continue; + if (i == client) continue; + + PushArrayCell(hPlayers, i); + } + + new iPlayerCount = GetArraySize(hPlayers); + if (iPlayerCount) + { + new Handle:hMenu = CreateMenu(Menu_KickFromGroup); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Kick From Group Menu Title", client, "SF2 Kick From Group Menu Description", client); + + decl String:sUserId[32]; + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < iPlayerCount; i++) + { + new iClient = GetArrayCell(hPlayers, i); + IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); + GetClientName(iClient, sName, sizeof(sName)); + AddMenuItem(hMenu, sUserId, sName); + } + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + } + else + { + // No players left for the taking! + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 No Players Available", client); + } + + CloseHandle(hPlayers); +} + +public Menu_KickFromGroup(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) + { + decl String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + new userid = StringToInt(sInfo); + new iPlayer = GetClientOfUserId(userid); + + if (ClientGetPlayerGroup(iPlayer) == iGroupIndex) + { + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + decl String:sName[MAX_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sGroupName, sizeof(sGroupName)); + GetClientName(iPlayer, sName, sizeof(sName)); + + CPrintToChat(iPlayer, "%T", "SF2 Kicked From Group", iPlayer, sGroupName); + ClientSetPlayerGroup(iPlayer, -1); + CPrintToChat(param1, "%T", "SF2 Player Kicked From Group", param1, sName); + } + else + { + CPrintToChat(param1, "%T", "SF2 Player Not In Group", param1); + } + } + + DisplayKickFromGroupMenuToClient(param1); + } +} + +DisplaySetGroupNameMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return; + } + + new Handle:hMenu = CreateMenu(Menu_SetGroupName); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Set Group Name Menu Title", client, "SF2 Set Group Name Menu Description", client); + + decl String:sBuffer[256]; + Format(sBuffer, sizeof(sBuffer), "%T", "Back", client); + AddMenuItem(hMenu, "0", sBuffer); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); +} + +public Menu_SetGroupName(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) DisplayAdminGroupMenuToClient(param1); +} + +DisplayInviteToGroupMenuToClient(client) +{ + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + // His group isn't valid anymore. Take him back to the main menu. + DisplayGroupMainMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return; + } + + if (GetPlayerGroupMemberCount(iGroupIndex) >= GetMaxPlayersForRound()) + { + // His group is full! + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 Group Is Full", client); + return; + } + + new Handle:hPlayers = CreateArray(); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i) || !IsClientParticipating(i)) continue; + + new iTempGroup = ClientGetPlayerGroup(i); + if (IsPlayerGroupActive(iTempGroup)) continue; + if (!g_bPlayerEliminated[i]) continue; + + PushArrayCell(hPlayers, i); + } + + new iPlayerCount = GetArraySize(hPlayers); + if (iPlayerCount) + { + new Handle:hMenu = CreateMenu(Menu_InviteToGroup); + SetMenuTitle(hMenu, "%t%T\n \n%T\n \n", "SF2 Prefix", "SF2 Invite To Group Menu Title", client, "SF2 Invite To Group Menu Description", client); + + decl String:sUserId[32]; + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < iPlayerCount; i++) + { + new iClient = GetArrayCell(hPlayers, i); + IntToString(GetClientUserId(iClient), sUserId, sizeof(sUserId)); + GetClientName(iClient, sName, sizeof(sName)); + AddMenuItem(hMenu, sUserId, sName); + } + + SetMenuExitBackButton(hMenu, true); + DisplayMenu(hMenu, client, MENU_TIME_FOREVER); + } + else + { + // No players left for the taking! + DisplayAdminGroupMenuToClient(client); + CPrintToChat(client, "%T", "SF2 No Players Available", client); + } + + CloseHandle(hPlayers); +} + +public Menu_InviteToGroup(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_End) CloseHandle(menu); + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) DisplayAdminGroupMenuToClient(param1); + } + else if (action == MenuAction_Select) + { + new iGroupIndex = ClientGetPlayerGroup(param1); + if (IsPlayerGroupActive(iGroupIndex) && GetPlayerGroupLeader(iGroupIndex) == param1) + { + decl String:sInfo[64]; + GetMenuItem(menu, param2, sInfo, sizeof(sInfo)); + new userid = StringToInt(sInfo); + new iInvitedPlayer = GetClientOfUserId(userid); + SendPlayerGroupInvitation(iInvitedPlayer, GetPlayerGroupID(iGroupIndex), param1); + } + + DisplayInviteToGroupMenuToClient(param1); + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/profiles.sp b/addons/sourcemod/scripting/rytp_horror/profiles.sp index 7ebe648..54ca036 100644 --- a/addons/sourcemod/scripting/rytp_horror/profiles.sp +++ b/addons/sourcemod/scripting/rytp_horror/profiles.sp @@ -1,1189 +1,1189 @@ -#if defined _sf2_profiles_included - #endinput -#endif -#define _sf2_profiles_included - -#define FILE_PROFILES "configs/sf2/profiles.cfg" -#define FILE_PROFILES_PACKS "configs/sf2/profiles_packs.cfg" -#define FILE_PROFILES_PACKS_DIR "configs/sf2/profiles/packs" - -static Handle:g_hBossProfileList = INVALID_HANDLE; -static Handle:g_hSelectableBossProfileList = INVALID_HANDLE; - -static Handle:g_hBossProfileNames = INVALID_HANDLE; -static Handle:g_hBossProfileData = INVALID_HANDLE; - -new Handle:g_cvBossProfilePack = INVALID_HANDLE; -new Handle:g_cvBossProfilePackDefault = INVALID_HANDLE; - -new Handle:g_hBossPackConfig = INVALID_HANDLE; - -new Handle:g_cvBossPackEndOfMapVote; -new Handle:g_cvBossPackVoteStartTime; -new Handle:g_cvBossPackVoteStartRound; -new Handle:g_cvBossPackVoteShuffle; - -static bool:g_bBossPackVoteEnabled = false; - -#if defined METHODMAPS - -methodmap SF2BossProfile -{ - property int Index - { - public get() { return _:this; } - } - - property int UniqueProfileIndex - { - public get() { return GetBossProfileUniqueProfileIndex(this.Index); } - } - - property int Skin - { - public get() { return GetBossProfileSkin(this.Index); } - } - - property int BodyGroups - { - public get() { return GetBossProfileBodyGroups(this.Index); } - } - - property float ModelScale - { - public get() { return GetBossProfileModelScale(this.Index); } - } - - property int Type - { - public get() { return GetBossProfileType(this.Index); } - } - - property int Flags - { - public get() { return GetBossProfileFlags(this.Index); } - } - - property float SearchRadius - { - public get() { return GetBossProfileSearchRadius(this.Index); } - } - - property float FOV - { - public get() { return GetBossProfileFOV(this.Index); } - } - - property float TurnRate - { - public get() { return GetBossProfileTurnRate(this.Index); } - } - - property float AngerStart - { - public get() { return GetBossProfileAngerStart(this.Index); } - } - - property float AngerAddOnPageGrab - { - public get() { return GetBossProfileAngerAddOnPageGrab(this.Index); } - } - - property float AngerAddOnPageGrabTimeDiff - { - public get() { return GetBossProfileAngerPageGrabTimeDiff(this.Index); } - } - - property float InstantKillRadius - { - public get() { return GetBossProfileInstantKillRadius(this.Index); } - } - - property float ScareRadius - { - public get() { return GetBossProfileScareRadius(this.Index); } - } - - property float ScareCooldown - { - public get() { return GetBossProfileScareCooldown(this.Index); } - } - - property int TeleportType - { - public get() { return GetBossProfileTeleportType(this.Index); } - } - - public float GetSpeed(int difficulty) - { - return GetBossProfileSpeed(this.Index, difficulty); - } - - public float GetMaxSpeed(int difficulty) - { - return GetBossProfileMaxSpeed(this.Index, difficulty); - } - - public void GetEyePositionOffset(float buffer[3]) - { - GetBossProfileEyePositionOffset(this.Index, buffer); - } - - public void GetEyeAngleOffset(float buffer[3]) - { - GetBossProfileEyeAngleOffset(this.Index, buffer); - } -} - -#endif - -#include "rytp_horror/profiles/profile_chaser.sp" - -enum -{ - BossProfileData_UniqueProfileIndex, - BossProfileData_Type, - BossProfileData_ModelScale, - BossProfileData_Skin, - BossProfileData_Body, - BossProfileData_Flags, - - BossProfileData_SpeedEasy, - BossProfileData_SpeedNormal, - BossProfileData_SpeedHard, - BossProfileData_SpeedInsane, - - BossProfileData_WalkSpeedEasy, - BossProfileData_WalkSpeedNormal, - BossProfileData_WalkSpeedHard, - BossProfileData_WalkSpeedInsane, - - BossProfileData_AirSpeedEasy, - BossProfileData_AirSpeedNormal, - BossProfileData_AirSpeedHard, - BossProfileData_AirSpeedInsane, - - BossProfileData_MaxSpeedEasy, - BossProfileData_MaxSpeedNormal, - BossProfileData_MaxSpeedHard, - BossProfileData_MaxSpeedInsane, - - BossProfileData_MaxWalkSpeedEasy, - BossProfileData_MaxWalkSpeedNormal, - BossProfileData_MaxWalkSpeedHard, - BossProfileData_MaxWalkSpeedInsane, - - BossProfileData_MaxAirSpeedEasy, - BossProfileData_MaxAirSpeedNormal, - BossProfileData_MaxAirSpeedHard, - BossProfileData_MaxAirSpeedInsane, - - BossProfileData_SearchRange, - BossProfileData_FieldOfView, - BossProfileData_TurnRate, - BossProfileData_EyePosOffsetX, - BossProfileData_EyePosOffsetY, - BossProfileData_EyePosOffsetZ, - BossProfileData_EyeAngOffsetX, - BossProfileData_EyeAngOffsetY, - BossProfileData_EyeAngOffsetZ, - BossProfileData_AngerStart, - BossProfileData_AngerAddOnPageGrab, - BossProfileData_AngerPageGrabTimeDiffReq, - BossProfileData_InstantKillRadius, - - BossProfileData_ScareRadius, - BossProfileData_ScareCooldown, - - BossProfileData_TeleportType, - BossProfileData_MaxStats -}; - -InitializeBossProfiles() -{ - g_hBossProfileNames = CreateTrie(); - g_hBossProfileData = CreateArray(BossProfileData_MaxStats); - - g_cvBossProfilePack = CreateConVar("sf2_boss_profile_pack", "", "The boss pack referenced in profiles_packs.cfg that should be loaded.", FCVAR_NOTIFY | FCVAR_DONTRECORD); - g_cvBossProfilePackDefault = CreateConVar("sf2_boss_profile_pack_default", "", "If the boss pack defined in sf2_boss_profile_pack is blank or could not be loaded, this pack will be used instead.", FCVAR_NOTIFY); - g_cvBossPackEndOfMapVote = CreateConVar("sf2_boss_profile_pack_endvote", "0", "Enables/Disables a boss pack vote at the end of the map."); - g_cvBossPackVoteStartTime = CreateConVar("sf2_boss_profile_pack_endvote_start", "4", "Specifies when to start the vote based on time remaining on the map, in minutes.", FCVAR_NOTIFY); - g_cvBossPackVoteStartRound = CreateConVar("sf2_boss_profile_pack_endvote_startround", "2", "Specifies when to start the vote based on rounds remaining on the map.", FCVAR_NOTIFY); - g_cvBossPackVoteShuffle = CreateConVar("sf2_boss_profile_pack_endvote_shuffle", "0", "Shuffles the menu options of boss pack endvotes if enabled."); - - InitializeChaserProfiles(); -} - -BossProfilesOnMapEnd() -{ - ClearBossProfiles(); -} - -/** - * Clears all data and memory currently in use by all boss profiles. - */ -ClearBossProfiles() -{ - if (g_hBossProfileList != INVALID_HANDLE) - { - CloseHandle(g_hBossProfileList); - g_hBossProfileList = INVALID_HANDLE; - } - - if (g_hSelectableBossProfileList != INVALID_HANDLE) - { - CloseHandle(g_hSelectableBossProfileList); - g_hSelectableBossProfileList = INVALID_HANDLE; - } - - ClearTrie(g_hBossProfileNames); - ClearArray(g_hBossProfileData); - - ClearChaserProfiles(); -} - -ReloadBossProfiles() -{ - if (g_hConfig != INVALID_HANDLE) - { - CloseHandle(g_hConfig); - g_hConfig = INVALID_HANDLE; - } - - if (g_hBossPackConfig != INVALID_HANDLE) - { - CloseHandle(g_hBossPackConfig); - g_hBossPackConfig = INVALID_HANDLE; - } - - // Clear and reload the lists. - ClearBossProfiles(); - - g_hConfig = CreateKeyValues("root"); - g_hBossPackConfig = CreateKeyValues("root"); - - if (g_hBossProfileList == INVALID_HANDLE) - { - g_hBossProfileList = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); - } - - if (g_hSelectableBossProfileList == INVALID_HANDLE) - { - g_hSelectableBossProfileList = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); - } - - decl String:configPath[PLATFORM_MAX_PATH]; - - // First load from configs/sf2/profiles.cfg - BuildPath(Path_SM, configPath, sizeof(configPath), FILE_PROFILES); - LoadProfilesFromFile(configPath); - - BuildPath(Path_SM, configPath, sizeof(configPath), FILE_PROFILES_PACKS); - FileToKeyValues(g_hBossPackConfig, configPath); - - g_bBossPackVoteEnabled = true; - - // Try loading boss packs, if they're set to load. - KvRewind(g_hBossPackConfig); - if (KvJumpToKey(g_hBossPackConfig, "packs")) - { - if (KvGotoFirstSubKey(g_hBossPackConfig)) - { - new endVoteItemCount = 0; - - decl String:forceLoadBossPackName[128]; - GetConVarString(g_cvBossProfilePack, forceLoadBossPackName, sizeof(forceLoadBossPackName)); - - new bool:voteBossPackLoaded = false; - - do - { - decl String:bossPackName[128]; - KvGetSectionName(g_hBossPackConfig, bossPackName, sizeof(bossPackName)); - - new bool:autoLoad = bool:KvGetNum(g_hBossPackConfig, "autoload"); - - if (autoLoad || (strlen(forceLoadBossPackName) > 0 && StrEqual(forceLoadBossPackName, bossPackName))) - { - decl String:packConfigFile[PLATFORM_MAX_PATH]; - KvGetString(g_hBossPackConfig, "file", packConfigFile, sizeof(packConfigFile)); - - decl String:packConfigFilePath[PLATFORM_MAX_PATH]; - Format(packConfigFilePath, sizeof(packConfigFilePath), "%s/%s", FILE_PROFILES_PACKS_DIR, packConfigFile); - - BuildPath(Path_SM, configPath, sizeof(configPath), packConfigFilePath); - LoadProfilesFromFile(configPath); - - if (!voteBossPackLoaded) - { - if (StrEqual(forceLoadBossPackName, bossPackName)) - { - voteBossPackLoaded = true; - } - } - } - - if (!autoLoad) - { - endVoteItemCount++; - } - } - while (KvGotoNextKey(g_hBossPackConfig)); - - KvGoBack(g_hBossPackConfig); - - if (!voteBossPackLoaded) - { - GetConVarString(g_cvBossProfilePackDefault, forceLoadBossPackName, sizeof(forceLoadBossPackName)); - if (strlen(forceLoadBossPackName) > 0) - { - if (KvJumpToKey(g_hBossPackConfig, forceLoadBossPackName)) - { - decl String:packConfigFile[PLATFORM_MAX_PATH]; - KvGetString(g_hBossPackConfig, "file", packConfigFile, sizeof(packConfigFile)); - - decl String:packConfigFilePath[PLATFORM_MAX_PATH]; - Format(packConfigFilePath, sizeof(packConfigFilePath), "%s/%s", FILE_PROFILES_PACKS_DIR, packConfigFile); - - BuildPath(Path_SM, configPath, sizeof(configPath), packConfigFilePath); - LoadProfilesFromFile(configPath); - } - } - } - - if (endVoteItemCount <= 0) - { - g_bBossPackVoteEnabled = false; - } - } - else - { - g_bBossPackVoteEnabled = false; - } - } - else - { - g_bBossPackVoteEnabled = false; - } -} - -static LoadProfilesFromFile(const String:configPath[]) -{ - LogSF2Message("Loading boss profiles from file %s...", configPath); - - if (!FileExists(configPath)) - { - LogSF2Message("File not found! Skipping..."); - return; - } - - new Handle:kv = CreateKeyValues("root"); - if (!FileToKeyValues(kv, configPath)) - { - CloseHandle(kv); - LogSF2Message("Unexpected error while reading file! Skipping..."); - return; - } - else - { - if (KvGotoFirstSubKey(kv)) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sProfileLoadFailReason[512]; - - new iLoadedCount = 0; - - do - { - KvGetSectionName(kv, sProfile, sizeof(sProfile)); - if (LoadBossProfile(kv, sProfile, sProfileLoadFailReason, sizeof(sProfileLoadFailReason))) - { - iLoadedCount++; - LogSF2Message("%s...", sProfile); - } - else - { - LogSF2Message("%s...FAILED (reason: %s)", sProfile, sProfileLoadFailReason); - } - } - while (KvGotoNextKey(kv)); - - LogSF2Message("Loaded %d boss profile(s) from file!", iLoadedCount); - } - else - { - LogSF2Message("No boss profiles loaded from file!"); - } - - CloseHandle(kv); - } -} - -/** - * Loads a profile in the current KeyValues position in kv. - */ -static bool:LoadBossProfile(Handle:kv, const String:sProfile[], String:sLoadFailReasonBuffer[], iLoadFailReasonBufferLen) -{ - new iBossType = KvGetNum(kv, "type", SF2BossType_Unknown); - if (iBossType == SF2BossType_Unknown || iBossType >= SF2BossType_MaxTypes) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "boss type is unknown!"); - return false; - } - - new Float:flBossModelScale = KvGetFloat(kv, "model_scale", 1.0); - if (flBossModelScale <= 0.0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "model_scale must be a value greater than 0!"); - return false; - } - - new iBossSkin = KvGetNum(kv, "skin"); - if (iBossSkin < 0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "skin must be a value that is at least 0!"); - return false; - } - - new iBossBodyGroups = KvGetNum(kv, "body"); - if (iBossBodyGroups < 0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "body must be a value that is at least 0!"); - return false; - } - - new Float:flBossAngerStart = KvGetFloat(kv, "anger_start", 1.0); - if (flBossAngerStart < 0.0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "anger_start must be a value that is at least 0!"); - return false; - } - - new Float:flBossInstantKillRadius = KvGetFloat(kv, "kill_radius"); - if (flBossInstantKillRadius < 0.0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "kill_radius must be a value that is at least 0!"); - return false; - } - - new Float:flBossScareRadius = KvGetFloat(kv, "scare_radius"); - if (flBossScareRadius < 0.0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "scare_radius must be a value that is at least 0!"); - return false; - } - - new iBossTeleportType = KvGetNum(kv, "teleport_type"); - if (iBossTeleportType < 0) - { - Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "unknown teleport type!"); - return false; - } - - new Float:flBossFOV = KvGetFloat(kv, "fov", 90.0); - if (flBossFOV < 0.0) - { - flBossFOV = 0.0; - } - else if (flBossFOV > 360.0) - { - flBossFOV = 360.0; - } - - new Float:flBossMaxTurnRate = KvGetFloat(kv, "turnrate", 90.0); - if (flBossMaxTurnRate < 0.0) - { - flBossMaxTurnRate = 0.0; - } - - new Float:flBossScareCooldown = KvGetFloat(kv, "scare_cooldown"); - if (flBossScareCooldown < 0.0) - { - // clamp value - flBossScareCooldown = 0.0; - } - - new Float:flBossAngerAddOnPageGrab = KvGetFloat(kv, "anger_add_on_page_grab", -1.0); - if (flBossAngerAddOnPageGrab < 0.0) - { - flBossAngerAddOnPageGrab = KvGetFloat(kv, "anger_page_add", -1.0); // backwards compatibility - if (flBossAngerAddOnPageGrab < 0.0) - { - flBossAngerAddOnPageGrab = 0.0; - } - } - - new Float:flBossAngerPageGrabTimeDiffReq = KvGetFloat(kv, "anger_req_page_grab_time_diff", -1.0); - if (flBossAngerPageGrabTimeDiffReq < 0.0) - { - flBossAngerPageGrabTimeDiffReq = KvGetFloat(kv, "anger_page_time_diff", -1.0); // backwards compatibility - if (flBossAngerPageGrabTimeDiffReq < 0.0) - { - flBossAngerPageGrabTimeDiffReq = 0.0; - } - } - - new Float:flBossSearchRadius = KvGetFloat(kv, "search_radius", -1.0); - if (flBossSearchRadius < 0.0) - { - flBossSearchRadius = KvGetFloat(kv, "search_range", -1.0); // backwards compatibility - if (flBossSearchRadius < 0.0) - { - flBossSearchRadius = 0.0; - } - } - - new Float:flBossDefaultSpeed = KvGetFloat(kv, "speed", 150.0); - new Float:flBossSpeedEasy = KvGetFloat(kv, "speed_easy", flBossDefaultSpeed); - new Float:flBossSpeedHard = KvGetFloat(kv, "speed_hard", flBossDefaultSpeed); - new Float:flBossSpeedInsane = KvGetFloat(kv, "speed_insane", flBossDefaultSpeed); - - new Float:flBossDefaultMaxSpeed = KvGetFloat(kv, "speed_max", 150.0); - new Float:flBossMaxSpeedEasy = KvGetFloat(kv, "speed_max_easy", flBossDefaultMaxSpeed); - new Float:flBossMaxSpeedHard = KvGetFloat(kv, "speed_max_hard", flBossDefaultMaxSpeed); - new Float:flBossMaxSpeedInsane = KvGetFloat(kv, "speed_max_insane", flBossDefaultMaxSpeed); - - decl Float:flBossEyePosOffset[3]; - KvGetVector(kv, "eye_pos", flBossEyePosOffset); - - decl Float:flBossEyeAngOffset[3]; - KvGetVector(kv, "eye_ang_offset", flBossEyeAngOffset); - - // Parse through flags. - new iBossFlags = 0; - if (KvGetNum(kv, "static_shake")) iBossFlags |= SFF_HASSTATICSHAKE; - if (KvGetNum(kv, "static_on_look")) iBossFlags |= SFF_STATICONLOOK; - if (KvGetNum(kv, "static_on_radius")) iBossFlags |= SFF_STATICONRADIUS; - if (KvGetNum(kv, "proxies")) iBossFlags |= SFF_PROXIES; - if (KvGetNum(kv, "jumpscare")) iBossFlags |= SFF_HASJUMPSCARE; - if (KvGetNum(kv, "sound_sight_enabled")) iBossFlags |= SFF_HASSIGHTSOUNDS; - if (KvGetNum(kv, "sound_static_loop_local_enabled")) iBossFlags |= SFF_HASSTATICLOOPLOCALSOUND; - if (KvGetNum(kv, "view_shake", 1)) iBossFlags |= SFF_HASVIEWSHAKE; - if (KvGetNum(kv, "copy")) iBossFlags |= SFF_COPIES; - if (KvGetNum(kv, "wander_move", 1)) iBossFlags |= SFF_WANDERMOVE; - - // Try validating unique profile. - new iUniqueProfileIndex = -1; - - switch (iBossType) - { - case SF2BossType_Chaser: - { - if (!LoadChaserBossProfile(kv, sProfile, iUniqueProfileIndex, sLoadFailReasonBuffer, iLoadFailReasonBufferLen)) - { - return false; - } - } - } - - // Add the section to our config. - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile, true); - KvCopySubkeys(kv, g_hConfig); - - new bool:createNewBoss = false; - new iIndex = FindStringInArray(GetBossProfileList(), sProfile); - if (iIndex == -1) - { - createNewBoss = true; - } - - // Add to/Modify our array. - if (createNewBoss) - { - iIndex = PushArrayCell(g_hBossProfileData, -1); - SetTrieValue(g_hBossProfileNames, sProfile, iIndex); - - // Add to the boss list since it's not there already. - PushArrayString(GetBossProfileList(), sProfile); - } - - SetArrayCell(g_hBossProfileData, iIndex, iUniqueProfileIndex, BossProfileData_UniqueProfileIndex); - - SetArrayCell(g_hBossProfileData, iIndex, iBossType, BossProfileData_Type); - SetArrayCell(g_hBossProfileData, iIndex, flBossModelScale, BossProfileData_ModelScale); - SetArrayCell(g_hBossProfileData, iIndex, iBossSkin, BossProfileData_Skin); - SetArrayCell(g_hBossProfileData, iIndex, iBossBodyGroups, BossProfileData_Body); - - SetArrayCell(g_hBossProfileData, iIndex, iBossFlags, BossProfileData_Flags); - - SetArrayCell(g_hBossProfileData, iIndex, flBossDefaultSpeed, BossProfileData_SpeedNormal); - SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedEasy, BossProfileData_SpeedEasy); - SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedHard, BossProfileData_SpeedHard); - SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedInsane, BossProfileData_SpeedInsane); - - SetArrayCell(g_hBossProfileData, iIndex, flBossDefaultMaxSpeed, BossProfileData_MaxSpeedNormal); - SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedEasy, BossProfileData_MaxSpeedEasy); - SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedHard, BossProfileData_MaxSpeedHard); - SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedInsane, BossProfileData_MaxSpeedInsane); - - SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[0], BossProfileData_EyePosOffsetX); - SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[1], BossProfileData_EyePosOffsetY); - SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[2], BossProfileData_EyePosOffsetZ); - - SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[0], BossProfileData_EyeAngOffsetX); - SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[1], BossProfileData_EyeAngOffsetY); - SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[2], BossProfileData_EyeAngOffsetZ); - - SetArrayCell(g_hBossProfileData, iIndex, flBossAngerStart, BossProfileData_AngerStart); - SetArrayCell(g_hBossProfileData, iIndex, flBossAngerAddOnPageGrab, BossProfileData_AngerAddOnPageGrab); - SetArrayCell(g_hBossProfileData, iIndex, flBossAngerPageGrabTimeDiffReq, BossProfileData_AngerPageGrabTimeDiffReq); - - SetArrayCell(g_hBossProfileData, iIndex, flBossInstantKillRadius, BossProfileData_InstantKillRadius); - - SetArrayCell(g_hBossProfileData, iIndex, flBossScareRadius, BossProfileData_ScareRadius); - SetArrayCell(g_hBossProfileData, iIndex, flBossScareCooldown, BossProfileData_ScareCooldown); - - SetArrayCell(g_hBossProfileData, iIndex, iBossTeleportType, BossProfileData_TeleportType); - - SetArrayCell(g_hBossProfileData, iIndex, flBossSearchRadius, BossProfileData_SearchRange); - SetArrayCell(g_hBossProfileData, iIndex, flBossFOV, BossProfileData_FieldOfView); - SetArrayCell(g_hBossProfileData, iIndex, flBossMaxTurnRate, BossProfileData_TurnRate); - - if (bool:KvGetNum(kv, "enable_random_selection", 1)) - { - if (FindStringInArray(GetSelectableBossProfileList(), sProfile) == -1) - { - // Add to the selectable boss list if it isn't there already. - PushArrayString(GetSelectableBossProfileList(), sProfile); - } - } - else - { - new selectIndex = FindStringInArray(GetSelectableBossProfileList(), sProfile); - if (selectIndex != -1) - { - RemoveFromArray(GetSelectableBossProfileList(), selectIndex); - } - } - - if (KvGotoFirstSubKey(kv)) - { - decl String:s2[64], String:s3[64], String:s4[PLATFORM_MAX_PATH], String:s5[PLATFORM_MAX_PATH]; - - do - { - KvGetSectionName(kv, s2, sizeof(s2)); - - if (!StrContains(s2, "sound_")) - { - for (new i = 1;; i++) - { - IntToString(i, s3, sizeof(s3)); - KvGetString(kv, s3, s4, sizeof(s4)); - if (!s4[0]) break; - - PrecacheSound2(s4); - } - } - else if (StrEqual(s2, "download")) - { - for (new i = 1;; i++) - { - IntToString(i, s3, sizeof(s3)); - KvGetString(kv, s3, s4, sizeof(s4)); - if (!s4[0]) break; - - AddFileToDownloadsTable(s4); - } - } - else if (StrEqual(s2, "mod_precache")) - { - for (new i = 1;; i++) - { - IntToString(i, s3, sizeof(s3)); - KvGetString(kv, s3, s4, sizeof(s4)); - if (!s4[0]) break; - - PrecacheModel(s4, true); - } - } - else if (StrEqual(s2, "mat_download")) - { - for (new i = 1;; i++) - { - IntToString(i, s3, sizeof(s3)); - KvGetString(kv, s3, s4, sizeof(s4)); - if (!s4[0]) break; - - Format(s5, sizeof(s5), "%s.vtf", s4); - AddFileToDownloadsTable(s5); - Format(s5, sizeof(s5), "%s.vmt", s4); - AddFileToDownloadsTable(s5); - } - } - else if (StrEqual(s2, "mod_download")) - { - static const String:extensions[][] = { ".mdl", ".phy", ".dx80.vtx", ".dx90.vtx", ".sw.vtx", ".vvd" }; - - for (new i = 1;; i++) - { - IntToString(i, s3, sizeof(s3)); - KvGetString(kv, s3, s4, sizeof(s4)); - if (!s4[0]) break; - - for (new is = 0; is < sizeof(extensions); is++) - { - Format(s5, sizeof(s5), "%s%s", s4, extensions[is]); - AddFileToDownloadsTable(s5); - } - } - } - } - while (KvGotoNextKey(kv)); - - KvGoBack(kv); - } - - return true; -} - -static Handle:g_hBossPackVoteMapTimer; -static Handle:g_hBossPackVoteTimer; -static bool:g_bBossPackVoteCompleted; -static bool:g_bBossPackVoteStarted; - -InitializeBossPackVotes() -{ - g_hBossPackVoteMapTimer = INVALID_HANDLE; - g_hBossPackVoteTimer = INVALID_HANDLE; - g_bBossPackVoteCompleted = false; - g_bBossPackVoteStarted = false; -} - -SetupTimeLimitTimerForBossPackVote() -{ - new time; - if (GetMapTimeLeft(time) && time > 0) - { - if (GetConVarBool(g_cvBossPackEndOfMapVote) && g_bBossPackVoteEnabled && !g_bBossPackVoteCompleted && !g_bBossPackVoteStarted) - { - new startTime = GetConVarInt(g_cvBossPackVoteStartTime) * 60; - if ((time - startTime) <= 0) - { - if (!IsVoteInProgress()) - { - InitiateBossPackVote(); - } - else - { - g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - } - else - { - if (g_hBossPackVoteMapTimer != INVALID_HANDLE) - { - CloseHandle(g_hBossPackVoteMapTimer); - g_hBossPackVoteMapTimer = INVALID_HANDLE; - } - - g_hBossPackVoteMapTimer = CreateTimer(float(time - startTime), Timer_StartBossPackVote, _, TIMER_FLAG_NO_MAPCHANGE); - } - } - } -} - -CheckRoundLimitForBossPackVote(roundCount) -{ - if (!GetConVarBool(g_cvBossPackEndOfMapVote) || !g_bBossPackVoteEnabled || g_bBossPackVoteStarted || g_bBossPackVoteCompleted) return; - - if (g_cvMaxRounds == INVALID_HANDLE) return; - - if (GetConVarInt(g_cvMaxRounds) > 0) - { - if (roundCount >= (GetConVarInt(g_cvMaxRounds) - GetConVarInt(g_cvBossPackVoteStartRound))) - { - if (!IsVoteInProgress()) - { - InitiateBossPackVote(); - } - else - { - g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - } - } - - CloseHandle(g_cvMaxRounds); -} - -InitiateBossPackVote() -{ - if (g_bBossPackVoteStarted || g_bBossPackVoteCompleted || IsVoteInProgress()) return; - - // Gather boss packs, if any. - if (g_hBossPackConfig == INVALID_HANDLE) return; - - KvRewind(g_hBossPackConfig); - if (!KvJumpToKey(g_hBossPackConfig, "packs")) return; - if (!KvGotoFirstSubKey(g_hBossPackConfig)) return; - - new Handle:voteMenu = CreateMenu(Menu_BossPackVote); - SetMenuTitle(voteMenu, "%t%t\n \n", "SF2 Prefix", "SF2 Boss Pack Vote Menu Title"); - SetMenuExitBackButton(voteMenu, false); - SetMenuExitButton(voteMenu, false); - - new Handle:menuDisplayNamesTrie = CreateTrie(); - new Handle:menuOptionsInfo = CreateArray(128); - - do - { - if (!bool:KvGetNum(g_hBossPackConfig, "autoload") && bool:KvGetNum(g_hBossPackConfig, "show_in_vote", 1)) - { - decl String:bossPack[128]; - KvGetSectionName(g_hBossPackConfig, bossPack, sizeof(bossPack)); - - decl String:bossPackName[64]; - KvGetString(g_hBossPackConfig, "name", bossPackName, sizeof(bossPackName), bossPack); - - SetTrieString(menuDisplayNamesTrie, bossPack, bossPackName); - PushArrayString(menuOptionsInfo, bossPack); - } - } - while (KvGotoNextKey(g_hBossPackConfig)); - - if (GetArraySize(menuOptionsInfo) == 0) - { - CloseHandle(menuDisplayNamesTrie); - CloseHandle(menuOptionsInfo); - CloseHandle(voteMenu); - return; - } - - if (GetConVarBool(g_cvBossPackVoteShuffle)) - { - SortADTArray(menuOptionsInfo, Sort_Random, Sort_String); - } - - for (new i = 0; i < GetArraySize(menuOptionsInfo); i++) - { - decl String:bossPack[128], String:bossPackName[64]; - GetArrayString(menuOptionsInfo, i, bossPack, sizeof(bossPack)); - GetTrieString(menuDisplayNamesTrie, bossPack, bossPackName, sizeof(bossPackName)); - - AddMenuItem(voteMenu, bossPack, bossPackName); - } - - CloseHandle(menuDisplayNamesTrie); - CloseHandle(menuOptionsInfo); - - g_bBossPackVoteStarted = true; - if (g_hBossPackVoteMapTimer != INVALID_HANDLE) - { - CloseHandle(g_hBossPackVoteMapTimer); - g_hBossPackVoteMapTimer = INVALID_HANDLE; - } - - if (g_hBossPackVoteTimer != INVALID_HANDLE) - { - CloseHandle(g_hBossPackVoteTimer); - g_hBossPackVoteTimer = INVALID_HANDLE; - } - - VoteMenuToAll(voteMenu, 20); -} - -public Menu_BossPackVote(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_VoteStart: - { - g_bBossPackVoteStarted = true; - } - case MenuAction_VoteEnd: - { - g_bBossPackVoteCompleted = true; - - decl String:bossPack[128], String:bossPackName[64]; - GetMenuItem(menu, param1, bossPack, sizeof(bossPack), _, bossPackName, sizeof(bossPackName)); - - SetConVarString(g_cvBossProfilePack, bossPack); - - CPrintToChatAll("%t%t", "SF2 Prefix", "SF2 Boss Pack Vote Successful", bossPackName); - } - case MenuAction_End: - { - g_bBossPackVoteStarted = false; - CloseHandle(menu); - } - } -} - -public Action:Timer_StartBossPackVote(Handle:timer) -{ - if (timer != g_hBossPackVoteMapTimer) return; - - g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hBossPackVoteTimer, true); -} - -public Action:Timer_BossPackVoteLoop(Handle:timer) -{ - if (timer != g_hBossPackVoteTimer || g_bBossPackVoteCompleted || g_bBossPackVoteStarted) return Plugin_Stop; - - if (!IsVoteInProgress()) - { - g_hBossPackVoteTimer = INVALID_HANDLE; - InitiateBossPackVote(); - return Plugin_Stop; - } - - return Plugin_Continue; -} - -bool:IsProfileValid(const String:sProfile[]) -{ - return bool:(FindStringInArray(GetBossProfileList(), sProfile) != -1); -} - -stock GetProfileNum(const String:sProfile[], const String:keyValue[], defaultValue=0) -{ - if (!IsProfileValid(sProfile)) return defaultValue; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - return KvGetNum(g_hConfig, keyValue, defaultValue); -} - -stock Float:GetProfileFloat(const String:sProfile[], const String:keyValue[], Float:defaultValue=0.0) -{ - if (!IsProfileValid(sProfile)) return defaultValue; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - return KvGetFloat(g_hConfig, keyValue, defaultValue); -} - -stock bool:GetProfileVector(const String:sProfile[], const String:keyValue[], Float:buffer[3], const Float:defaultValue[3]=NULL_VECTOR) -{ - for (new i = 0; i < 3; i++) buffer[i] = defaultValue[i]; - - if (!IsProfileValid(sProfile)) return false; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - KvGetVector(g_hConfig, keyValue, buffer, defaultValue); - return true; -} - -stock bool:GetProfileColor(const String:sProfile[], - const String:keyValue[], - &r, - &g, - &b, - &a, - dr=255, - dg=255, - db=255, - da=255) -{ - r = dr; - g = dg; - b = db; - a = da; - - if (!IsProfileValid(sProfile)) return false; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - decl String:sValue[64]; - KvGetString(g_hConfig, keyValue, sValue, sizeof(sValue)); - - if (strlen(sValue) != 0) - { - KvGetColor(g_hConfig, keyValue, r, g, b, a); - } - - return true; -} - -stock bool:GetProfileString(const String:sProfile[], const String:keyValue[], String:buffer[], bufferlen, const String:defaultValue[]="") -{ - strcopy(buffer, bufferlen, defaultValue); - - if (!IsProfileValid(sProfile)) return false; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - KvGetString(g_hConfig, keyValue, buffer, bufferlen, defaultValue); - return true; -} - -GetBossProfileIndexFromName(const String:sProfile[]) -{ - new iReturn = -1; - GetTrieValue(g_hBossProfileNames, sProfile, iReturn); - return iReturn; -} - -GetBossProfileUniqueProfileIndex(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_UniqueProfileIndex); -} - -GetBossProfileSkin(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Skin); -} - -GetBossProfileBodyGroups(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Body); -} - -Float:GetBossProfileModelScale(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ModelScale); -} - -GetBossProfileType(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Type); -} - -GetBossProfileFlags(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Flags); -} - -Float:GetBossProfileSpeed(iProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedEasy); - case Difficulty_Hard: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedHard); - case Difficulty_Insane: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedInsane); - } - - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedNormal); -} - -Float:GetBossProfileMaxSpeed(iProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedEasy); - case Difficulty_Hard: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedHard); - case Difficulty_Insane: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedInsane); - } - - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedNormal); -} - -Float:GetBossProfileSearchRadius(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SearchRange); -} - -Float:GetBossProfileFOV(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_FieldOfView); -} - -Float:GetBossProfileTurnRate(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_TurnRate); -} - -GetBossProfileEyePositionOffset(iProfileIndex, Float:buffer[3]) -{ - buffer[0] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetX); - buffer[1] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetY); - buffer[2] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetZ); -} - -GetBossProfileEyeAngleOffset(iProfileIndex, Float:buffer[3]) -{ - buffer[0] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetX); - buffer[1] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetY); - buffer[2] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetZ); -} - -Float:GetBossProfileAngerStart(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerStart); -} - -Float:GetBossProfileAngerAddOnPageGrab(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerAddOnPageGrab); -} - -Float:GetBossProfileAngerPageGrabTimeDiff(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerPageGrabTimeDiffReq); -} - -Float:GetBossProfileInstantKillRadius(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_InstantKillRadius); -} - -Float:GetBossProfileScareRadius(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ScareRadius); -} - -Float:GetBossProfileScareCooldown(iProfileIndex) -{ - return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ScareCooldown); -} - -GetBossProfileTeleportType(iProfileIndex) -{ - return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_TeleportType); -} - -// Code originally from FF2. Credits to the original authors Rainbolt Dash and FlaminSarge. -stock bool:GetRandomStringFromProfile(const String:sProfile[], const String:strKeyValue[], String:buffer[], bufferlen, index=-1) -{ - strcopy(buffer, bufferlen, ""); - - if (!IsProfileValid(sProfile)) return false; - - KvRewind(g_hConfig); - KvJumpToKey(g_hConfig, sProfile); - - if (!KvJumpToKey(g_hConfig, strKeyValue)) return false; - - decl String:s[32], String:s2[PLATFORM_MAX_PATH]; - - new i = 1; - for (;;) - { - IntToString(i, s, sizeof(s)); - KvGetString(g_hConfig, s, s2, sizeof(s2)); - if (!s2[0]) break; - - i++; - } - - if (i == 1) return false; - - IntToString(index < 0 ? GetRandomInt(1, i - 1) : index, s, sizeof(s)); - KvGetString(g_hConfig, s, buffer, bufferlen); - return true; -} - -/** - * Returns an array of strings of the profile names of every valid boss. - */ -Handle:GetBossProfileList() -{ - return g_hBossProfileList; -} - -/** - * Returns an array of strings of the profile names of every valid boss that can be randomly selected. - */ -Handle:GetSelectableBossProfileList() -{ - return g_hSelectableBossProfileList; +#if defined _sf2_profiles_included + #endinput +#endif +#define _sf2_profiles_included + +#define FILE_PROFILES "configs/sf2/profiles.cfg" +#define FILE_PROFILES_PACKS "configs/sf2/profiles_packs.cfg" +#define FILE_PROFILES_PACKS_DIR "configs/sf2/profiles/packs" + +static Handle:g_hBossProfileList = INVALID_HANDLE; +static Handle:g_hSelectableBossProfileList = INVALID_HANDLE; + +static Handle:g_hBossProfileNames = INVALID_HANDLE; +static Handle:g_hBossProfileData = INVALID_HANDLE; + +new Handle:g_cvBossProfilePack = INVALID_HANDLE; +new Handle:g_cvBossProfilePackDefault = INVALID_HANDLE; + +new Handle:g_hBossPackConfig = INVALID_HANDLE; + +new Handle:g_cvBossPackEndOfMapVote; +new Handle:g_cvBossPackVoteStartTime; +new Handle:g_cvBossPackVoteStartRound; +new Handle:g_cvBossPackVoteShuffle; + +static bool:g_bBossPackVoteEnabled = false; + +#if defined METHODMAPS + +methodmap SF2BossProfile +{ + property int Index + { + public get() { return _:this; } + } + + property int UniqueProfileIndex + { + public get() { return GetBossProfileUniqueProfileIndex(this.Index); } + } + + property int Skin + { + public get() { return GetBossProfileSkin(this.Index); } + } + + property int BodyGroups + { + public get() { return GetBossProfileBodyGroups(this.Index); } + } + + property float ModelScale + { + public get() { return GetBossProfileModelScale(this.Index); } + } + + property int Type + { + public get() { return GetBossProfileType(this.Index); } + } + + property int Flags + { + public get() { return GetBossProfileFlags(this.Index); } + } + + property float SearchRadius + { + public get() { return GetBossProfileSearchRadius(this.Index); } + } + + property float FOV + { + public get() { return GetBossProfileFOV(this.Index); } + } + + property float TurnRate + { + public get() { return GetBossProfileTurnRate(this.Index); } + } + + property float AngerStart + { + public get() { return GetBossProfileAngerStart(this.Index); } + } + + property float AngerAddOnPageGrab + { + public get() { return GetBossProfileAngerAddOnPageGrab(this.Index); } + } + + property float AngerAddOnPageGrabTimeDiff + { + public get() { return GetBossProfileAngerPageGrabTimeDiff(this.Index); } + } + + property float InstantKillRadius + { + public get() { return GetBossProfileInstantKillRadius(this.Index); } + } + + property float ScareRadius + { + public get() { return GetBossProfileScareRadius(this.Index); } + } + + property float ScareCooldown + { + public get() { return GetBossProfileScareCooldown(this.Index); } + } + + property int TeleportType + { + public get() { return GetBossProfileTeleportType(this.Index); } + } + + public float GetSpeed(int difficulty) + { + return GetBossProfileSpeed(this.Index, difficulty); + } + + public float GetMaxSpeed(int difficulty) + { + return GetBossProfileMaxSpeed(this.Index, difficulty); + } + + public void GetEyePositionOffset(float buffer[3]) + { + GetBossProfileEyePositionOffset(this.Index, buffer); + } + + public void GetEyeAngleOffset(float buffer[3]) + { + GetBossProfileEyeAngleOffset(this.Index, buffer); + } +} + +#endif + +#include "rytp_horror/profiles/profile_chaser.sp" + +enum +{ + BossProfileData_UniqueProfileIndex, + BossProfileData_Type, + BossProfileData_ModelScale, + BossProfileData_Skin, + BossProfileData_Body, + BossProfileData_Flags, + + BossProfileData_SpeedEasy, + BossProfileData_SpeedNormal, + BossProfileData_SpeedHard, + BossProfileData_SpeedInsane, + + BossProfileData_WalkSpeedEasy, + BossProfileData_WalkSpeedNormal, + BossProfileData_WalkSpeedHard, + BossProfileData_WalkSpeedInsane, + + BossProfileData_AirSpeedEasy, + BossProfileData_AirSpeedNormal, + BossProfileData_AirSpeedHard, + BossProfileData_AirSpeedInsane, + + BossProfileData_MaxSpeedEasy, + BossProfileData_MaxSpeedNormal, + BossProfileData_MaxSpeedHard, + BossProfileData_MaxSpeedInsane, + + BossProfileData_MaxWalkSpeedEasy, + BossProfileData_MaxWalkSpeedNormal, + BossProfileData_MaxWalkSpeedHard, + BossProfileData_MaxWalkSpeedInsane, + + BossProfileData_MaxAirSpeedEasy, + BossProfileData_MaxAirSpeedNormal, + BossProfileData_MaxAirSpeedHard, + BossProfileData_MaxAirSpeedInsane, + + BossProfileData_SearchRange, + BossProfileData_FieldOfView, + BossProfileData_TurnRate, + BossProfileData_EyePosOffsetX, + BossProfileData_EyePosOffsetY, + BossProfileData_EyePosOffsetZ, + BossProfileData_EyeAngOffsetX, + BossProfileData_EyeAngOffsetY, + BossProfileData_EyeAngOffsetZ, + BossProfileData_AngerStart, + BossProfileData_AngerAddOnPageGrab, + BossProfileData_AngerPageGrabTimeDiffReq, + BossProfileData_InstantKillRadius, + + BossProfileData_ScareRadius, + BossProfileData_ScareCooldown, + + BossProfileData_TeleportType, + BossProfileData_MaxStats +}; + +InitializeBossProfiles() +{ + g_hBossProfileNames = CreateTrie(); + g_hBossProfileData = CreateArray(BossProfileData_MaxStats); + + g_cvBossProfilePack = CreateConVar("sf2_boss_profile_pack", "", "The boss pack referenced in profiles_packs.cfg that should be loaded.", FCVAR_NOTIFY | FCVAR_DONTRECORD); + g_cvBossProfilePackDefault = CreateConVar("sf2_boss_profile_pack_default", "", "If the boss pack defined in sf2_boss_profile_pack is blank or could not be loaded, this pack will be used instead.", FCVAR_NOTIFY); + g_cvBossPackEndOfMapVote = CreateConVar("sf2_boss_profile_pack_endvote", "0", "Enables/Disables a boss pack vote at the end of the map."); + g_cvBossPackVoteStartTime = CreateConVar("sf2_boss_profile_pack_endvote_start", "4", "Specifies when to start the vote based on time remaining on the map, in minutes.", FCVAR_NOTIFY); + g_cvBossPackVoteStartRound = CreateConVar("sf2_boss_profile_pack_endvote_startround", "2", "Specifies when to start the vote based on rounds remaining on the map.", FCVAR_NOTIFY); + g_cvBossPackVoteShuffle = CreateConVar("sf2_boss_profile_pack_endvote_shuffle", "0", "Shuffles the menu options of boss pack endvotes if enabled."); + + InitializeChaserProfiles(); +} + +BossProfilesOnMapEnd() +{ + ClearBossProfiles(); +} + +/** + * Clears all data and memory currently in use by all boss profiles. + */ +ClearBossProfiles() +{ + if (g_hBossProfileList != INVALID_HANDLE) + { + CloseHandle(g_hBossProfileList); + g_hBossProfileList = INVALID_HANDLE; + } + + if (g_hSelectableBossProfileList != INVALID_HANDLE) + { + CloseHandle(g_hSelectableBossProfileList); + g_hSelectableBossProfileList = INVALID_HANDLE; + } + + ClearTrie(g_hBossProfileNames); + ClearArray(g_hBossProfileData); + + ClearChaserProfiles(); +} + +ReloadBossProfiles() +{ + if (g_hConfig != INVALID_HANDLE) + { + CloseHandle(g_hConfig); + g_hConfig = INVALID_HANDLE; + } + + if (g_hBossPackConfig != INVALID_HANDLE) + { + CloseHandle(g_hBossPackConfig); + g_hBossPackConfig = INVALID_HANDLE; + } + + // Clear and reload the lists. + ClearBossProfiles(); + + g_hConfig = CreateKeyValues("root"); + g_hBossPackConfig = CreateKeyValues("root"); + + if (g_hBossProfileList == INVALID_HANDLE) + { + g_hBossProfileList = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); + } + + if (g_hSelectableBossProfileList == INVALID_HANDLE) + { + g_hSelectableBossProfileList = CreateArray(SF2_MAX_PROFILE_NAME_LENGTH); + } + + decl String:configPath[PLATFORM_MAX_PATH]; + + // First load from configs/sf2/profiles.cfg + BuildPath(Path_SM, configPath, sizeof(configPath), FILE_PROFILES); + LoadProfilesFromFile(configPath); + + BuildPath(Path_SM, configPath, sizeof(configPath), FILE_PROFILES_PACKS); + FileToKeyValues(g_hBossPackConfig, configPath); + + g_bBossPackVoteEnabled = true; + + // Try loading boss packs, if they're set to load. + KvRewind(g_hBossPackConfig); + if (KvJumpToKey(g_hBossPackConfig, "packs")) + { + if (KvGotoFirstSubKey(g_hBossPackConfig)) + { + new endVoteItemCount = 0; + + decl String:forceLoadBossPackName[128]; + GetConVarString(g_cvBossProfilePack, forceLoadBossPackName, sizeof(forceLoadBossPackName)); + + new bool:voteBossPackLoaded = false; + + do + { + decl String:bossPackName[128]; + KvGetSectionName(g_hBossPackConfig, bossPackName, sizeof(bossPackName)); + + new bool:autoLoad = bool:KvGetNum(g_hBossPackConfig, "autoload"); + + if (autoLoad || (strlen(forceLoadBossPackName) > 0 && StrEqual(forceLoadBossPackName, bossPackName))) + { + decl String:packConfigFile[PLATFORM_MAX_PATH]; + KvGetString(g_hBossPackConfig, "file", packConfigFile, sizeof(packConfigFile)); + + decl String:packConfigFilePath[PLATFORM_MAX_PATH]; + Format(packConfigFilePath, sizeof(packConfigFilePath), "%s/%s", FILE_PROFILES_PACKS_DIR, packConfigFile); + + BuildPath(Path_SM, configPath, sizeof(configPath), packConfigFilePath); + LoadProfilesFromFile(configPath); + + if (!voteBossPackLoaded) + { + if (StrEqual(forceLoadBossPackName, bossPackName)) + { + voteBossPackLoaded = true; + } + } + } + + if (!autoLoad) + { + endVoteItemCount++; + } + } + while (KvGotoNextKey(g_hBossPackConfig)); + + KvGoBack(g_hBossPackConfig); + + if (!voteBossPackLoaded) + { + GetConVarString(g_cvBossProfilePackDefault, forceLoadBossPackName, sizeof(forceLoadBossPackName)); + if (strlen(forceLoadBossPackName) > 0) + { + if (KvJumpToKey(g_hBossPackConfig, forceLoadBossPackName)) + { + decl String:packConfigFile[PLATFORM_MAX_PATH]; + KvGetString(g_hBossPackConfig, "file", packConfigFile, sizeof(packConfigFile)); + + decl String:packConfigFilePath[PLATFORM_MAX_PATH]; + Format(packConfigFilePath, sizeof(packConfigFilePath), "%s/%s", FILE_PROFILES_PACKS_DIR, packConfigFile); + + BuildPath(Path_SM, configPath, sizeof(configPath), packConfigFilePath); + LoadProfilesFromFile(configPath); + } + } + } + + if (endVoteItemCount <= 0) + { + g_bBossPackVoteEnabled = false; + } + } + else + { + g_bBossPackVoteEnabled = false; + } + } + else + { + g_bBossPackVoteEnabled = false; + } +} + +static LoadProfilesFromFile(const String:configPath[]) +{ + LogSF2Message("Loading boss profiles from file %s...", configPath); + + if (!FileExists(configPath)) + { + LogSF2Message("File not found! Skipping..."); + return; + } + + new Handle:kv = CreateKeyValues("root"); + if (!FileToKeyValues(kv, configPath)) + { + CloseHandle(kv); + LogSF2Message("Unexpected error while reading file! Skipping..."); + return; + } + else + { + if (KvGotoFirstSubKey(kv)) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sProfileLoadFailReason[512]; + + new iLoadedCount = 0; + + do + { + KvGetSectionName(kv, sProfile, sizeof(sProfile)); + if (LoadBossProfile(kv, sProfile, sProfileLoadFailReason, sizeof(sProfileLoadFailReason))) + { + iLoadedCount++; + LogSF2Message("%s...", sProfile); + } + else + { + LogSF2Message("%s...FAILED (reason: %s)", sProfile, sProfileLoadFailReason); + } + } + while (KvGotoNextKey(kv)); + + LogSF2Message("Loaded %d boss profile(s) from file!", iLoadedCount); + } + else + { + LogSF2Message("No boss profiles loaded from file!"); + } + + CloseHandle(kv); + } +} + +/** + * Loads a profile in the current KeyValues position in kv. + */ +static bool:LoadBossProfile(Handle:kv, const String:sProfile[], String:sLoadFailReasonBuffer[], iLoadFailReasonBufferLen) +{ + new iBossType = KvGetNum(kv, "type", SF2BossType_Unknown); + if (iBossType == SF2BossType_Unknown || iBossType >= SF2BossType_MaxTypes) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "boss type is unknown!"); + return false; + } + + new Float:flBossModelScale = KvGetFloat(kv, "model_scale", 1.0); + if (flBossModelScale <= 0.0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "model_scale must be a value greater than 0!"); + return false; + } + + new iBossSkin = KvGetNum(kv, "skin"); + if (iBossSkin < 0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "skin must be a value that is at least 0!"); + return false; + } + + new iBossBodyGroups = KvGetNum(kv, "body"); + if (iBossBodyGroups < 0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "body must be a value that is at least 0!"); + return false; + } + + new Float:flBossAngerStart = KvGetFloat(kv, "anger_start", 1.0); + if (flBossAngerStart < 0.0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "anger_start must be a value that is at least 0!"); + return false; + } + + new Float:flBossInstantKillRadius = KvGetFloat(kv, "kill_radius"); + if (flBossInstantKillRadius < 0.0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "kill_radius must be a value that is at least 0!"); + return false; + } + + new Float:flBossScareRadius = KvGetFloat(kv, "scare_radius"); + if (flBossScareRadius < 0.0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "scare_radius must be a value that is at least 0!"); + return false; + } + + new iBossTeleportType = KvGetNum(kv, "teleport_type"); + if (iBossTeleportType < 0) + { + Format(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, "unknown teleport type!"); + return false; + } + + new Float:flBossFOV = KvGetFloat(kv, "fov", 90.0); + if (flBossFOV < 0.0) + { + flBossFOV = 0.0; + } + else if (flBossFOV > 360.0) + { + flBossFOV = 360.0; + } + + new Float:flBossMaxTurnRate = KvGetFloat(kv, "turnrate", 90.0); + if (flBossMaxTurnRate < 0.0) + { + flBossMaxTurnRate = 0.0; + } + + new Float:flBossScareCooldown = KvGetFloat(kv, "scare_cooldown"); + if (flBossScareCooldown < 0.0) + { + // clamp value + flBossScareCooldown = 0.0; + } + + new Float:flBossAngerAddOnPageGrab = KvGetFloat(kv, "anger_add_on_page_grab", -1.0); + if (flBossAngerAddOnPageGrab < 0.0) + { + flBossAngerAddOnPageGrab = KvGetFloat(kv, "anger_page_add", -1.0); // backwards compatibility + if (flBossAngerAddOnPageGrab < 0.0) + { + flBossAngerAddOnPageGrab = 0.0; + } + } + + new Float:flBossAngerPageGrabTimeDiffReq = KvGetFloat(kv, "anger_req_page_grab_time_diff", -1.0); + if (flBossAngerPageGrabTimeDiffReq < 0.0) + { + flBossAngerPageGrabTimeDiffReq = KvGetFloat(kv, "anger_page_time_diff", -1.0); // backwards compatibility + if (flBossAngerPageGrabTimeDiffReq < 0.0) + { + flBossAngerPageGrabTimeDiffReq = 0.0; + } + } + + new Float:flBossSearchRadius = KvGetFloat(kv, "search_radius", -1.0); + if (flBossSearchRadius < 0.0) + { + flBossSearchRadius = KvGetFloat(kv, "search_range", -1.0); // backwards compatibility + if (flBossSearchRadius < 0.0) + { + flBossSearchRadius = 0.0; + } + } + + new Float:flBossDefaultSpeed = KvGetFloat(kv, "speed", 150.0); + new Float:flBossSpeedEasy = KvGetFloat(kv, "speed_easy", flBossDefaultSpeed); + new Float:flBossSpeedHard = KvGetFloat(kv, "speed_hard", flBossDefaultSpeed); + new Float:flBossSpeedInsane = KvGetFloat(kv, "speed_insane", flBossDefaultSpeed); + + new Float:flBossDefaultMaxSpeed = KvGetFloat(kv, "speed_max", 150.0); + new Float:flBossMaxSpeedEasy = KvGetFloat(kv, "speed_max_easy", flBossDefaultMaxSpeed); + new Float:flBossMaxSpeedHard = KvGetFloat(kv, "speed_max_hard", flBossDefaultMaxSpeed); + new Float:flBossMaxSpeedInsane = KvGetFloat(kv, "speed_max_insane", flBossDefaultMaxSpeed); + + decl Float:flBossEyePosOffset[3]; + KvGetVector(kv, "eye_pos", flBossEyePosOffset); + + decl Float:flBossEyeAngOffset[3]; + KvGetVector(kv, "eye_ang_offset", flBossEyeAngOffset); + + // Parse through flags. + new iBossFlags = 0; + if (KvGetNum(kv, "static_shake")) iBossFlags |= SFF_HASSTATICSHAKE; + if (KvGetNum(kv, "static_on_look")) iBossFlags |= SFF_STATICONLOOK; + if (KvGetNum(kv, "static_on_radius")) iBossFlags |= SFF_STATICONRADIUS; + if (KvGetNum(kv, "proxies")) iBossFlags |= SFF_PROXIES; + if (KvGetNum(kv, "jumpscare")) iBossFlags |= SFF_HASJUMPSCARE; + if (KvGetNum(kv, "sound_sight_enabled")) iBossFlags |= SFF_HASSIGHTSOUNDS; + if (KvGetNum(kv, "sound_static_loop_local_enabled")) iBossFlags |= SFF_HASSTATICLOOPLOCALSOUND; + if (KvGetNum(kv, "view_shake", 1)) iBossFlags |= SFF_HASVIEWSHAKE; + if (KvGetNum(kv, "copy")) iBossFlags |= SFF_COPIES; + if (KvGetNum(kv, "wander_move", 1)) iBossFlags |= SFF_WANDERMOVE; + + // Try validating unique profile. + new iUniqueProfileIndex = -1; + + switch (iBossType) + { + case SF2BossType_Chaser: + { + if (!LoadChaserBossProfile(kv, sProfile, iUniqueProfileIndex, sLoadFailReasonBuffer, iLoadFailReasonBufferLen)) + { + return false; + } + } + } + + // Add the section to our config. + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile, true); + KvCopySubkeys(kv, g_hConfig); + + new bool:createNewBoss = false; + new iIndex = FindStringInArray(GetBossProfileList(), sProfile); + if (iIndex == -1) + { + createNewBoss = true; + } + + // Add to/Modify our array. + if (createNewBoss) + { + iIndex = PushArrayCell(g_hBossProfileData, -1); + SetTrieValue(g_hBossProfileNames, sProfile, iIndex); + + // Add to the boss list since it's not there already. + PushArrayString(GetBossProfileList(), sProfile); + } + + SetArrayCell(g_hBossProfileData, iIndex, iUniqueProfileIndex, BossProfileData_UniqueProfileIndex); + + SetArrayCell(g_hBossProfileData, iIndex, iBossType, BossProfileData_Type); + SetArrayCell(g_hBossProfileData, iIndex, flBossModelScale, BossProfileData_ModelScale); + SetArrayCell(g_hBossProfileData, iIndex, iBossSkin, BossProfileData_Skin); + SetArrayCell(g_hBossProfileData, iIndex, iBossBodyGroups, BossProfileData_Body); + + SetArrayCell(g_hBossProfileData, iIndex, iBossFlags, BossProfileData_Flags); + + SetArrayCell(g_hBossProfileData, iIndex, flBossDefaultSpeed, BossProfileData_SpeedNormal); + SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedEasy, BossProfileData_SpeedEasy); + SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedHard, BossProfileData_SpeedHard); + SetArrayCell(g_hBossProfileData, iIndex, flBossSpeedInsane, BossProfileData_SpeedInsane); + + SetArrayCell(g_hBossProfileData, iIndex, flBossDefaultMaxSpeed, BossProfileData_MaxSpeedNormal); + SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedEasy, BossProfileData_MaxSpeedEasy); + SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedHard, BossProfileData_MaxSpeedHard); + SetArrayCell(g_hBossProfileData, iIndex, flBossMaxSpeedInsane, BossProfileData_MaxSpeedInsane); + + SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[0], BossProfileData_EyePosOffsetX); + SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[1], BossProfileData_EyePosOffsetY); + SetArrayCell(g_hBossProfileData, iIndex, flBossEyePosOffset[2], BossProfileData_EyePosOffsetZ); + + SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[0], BossProfileData_EyeAngOffsetX); + SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[1], BossProfileData_EyeAngOffsetY); + SetArrayCell(g_hBossProfileData, iIndex, flBossEyeAngOffset[2], BossProfileData_EyeAngOffsetZ); + + SetArrayCell(g_hBossProfileData, iIndex, flBossAngerStart, BossProfileData_AngerStart); + SetArrayCell(g_hBossProfileData, iIndex, flBossAngerAddOnPageGrab, BossProfileData_AngerAddOnPageGrab); + SetArrayCell(g_hBossProfileData, iIndex, flBossAngerPageGrabTimeDiffReq, BossProfileData_AngerPageGrabTimeDiffReq); + + SetArrayCell(g_hBossProfileData, iIndex, flBossInstantKillRadius, BossProfileData_InstantKillRadius); + + SetArrayCell(g_hBossProfileData, iIndex, flBossScareRadius, BossProfileData_ScareRadius); + SetArrayCell(g_hBossProfileData, iIndex, flBossScareCooldown, BossProfileData_ScareCooldown); + + SetArrayCell(g_hBossProfileData, iIndex, iBossTeleportType, BossProfileData_TeleportType); + + SetArrayCell(g_hBossProfileData, iIndex, flBossSearchRadius, BossProfileData_SearchRange); + SetArrayCell(g_hBossProfileData, iIndex, flBossFOV, BossProfileData_FieldOfView); + SetArrayCell(g_hBossProfileData, iIndex, flBossMaxTurnRate, BossProfileData_TurnRate); + + if (bool:KvGetNum(kv, "enable_random_selection", 1)) + { + if (FindStringInArray(GetSelectableBossProfileList(), sProfile) == -1) + { + // Add to the selectable boss list if it isn't there already. + PushArrayString(GetSelectableBossProfileList(), sProfile); + } + } + else + { + new selectIndex = FindStringInArray(GetSelectableBossProfileList(), sProfile); + if (selectIndex != -1) + { + RemoveFromArray(GetSelectableBossProfileList(), selectIndex); + } + } + + if (KvGotoFirstSubKey(kv)) + { + decl String:s2[64], String:s3[64], String:s4[PLATFORM_MAX_PATH], String:s5[PLATFORM_MAX_PATH]; + + do + { + KvGetSectionName(kv, s2, sizeof(s2)); + + if (!StrContains(s2, "sound_")) + { + for (new i = 1;; i++) + { + IntToString(i, s3, sizeof(s3)); + KvGetString(kv, s3, s4, sizeof(s4)); + if (!s4[0]) break; + + PrecacheSound2(s4); + } + } + else if (StrEqual(s2, "download")) + { + for (new i = 1;; i++) + { + IntToString(i, s3, sizeof(s3)); + KvGetString(kv, s3, s4, sizeof(s4)); + if (!s4[0]) break; + + AddFileToDownloadsTable(s4); + } + } + else if (StrEqual(s2, "mod_precache")) + { + for (new i = 1;; i++) + { + IntToString(i, s3, sizeof(s3)); + KvGetString(kv, s3, s4, sizeof(s4)); + if (!s4[0]) break; + + PrecacheModel(s4, true); + } + } + else if (StrEqual(s2, "mat_download")) + { + for (new i = 1;; i++) + { + IntToString(i, s3, sizeof(s3)); + KvGetString(kv, s3, s4, sizeof(s4)); + if (!s4[0]) break; + + Format(s5, sizeof(s5), "%s.vtf", s4); + AddFileToDownloadsTable(s5); + Format(s5, sizeof(s5), "%s.vmt", s4); + AddFileToDownloadsTable(s5); + } + } + else if (StrEqual(s2, "mod_download")) + { + static const String:extensions[][] = { ".mdl", ".phy", ".dx80.vtx", ".dx90.vtx", ".sw.vtx", ".vvd" }; + + for (new i = 1;; i++) + { + IntToString(i, s3, sizeof(s3)); + KvGetString(kv, s3, s4, sizeof(s4)); + if (!s4[0]) break; + + for (new is = 0; is < sizeof(extensions); is++) + { + Format(s5, sizeof(s5), "%s%s", s4, extensions[is]); + AddFileToDownloadsTable(s5); + } + } + } + } + while (KvGotoNextKey(kv)); + + KvGoBack(kv); + } + + return true; +} + +static Handle:g_hBossPackVoteMapTimer; +static Handle:g_hBossPackVoteTimer; +static bool:g_bBossPackVoteCompleted; +static bool:g_bBossPackVoteStarted; + +InitializeBossPackVotes() +{ + g_hBossPackVoteMapTimer = INVALID_HANDLE; + g_hBossPackVoteTimer = INVALID_HANDLE; + g_bBossPackVoteCompleted = false; + g_bBossPackVoteStarted = false; +} + +SetupTimeLimitTimerForBossPackVote() +{ + new time; + if (GetMapTimeLeft(time) && time > 0) + { + if (GetConVarBool(g_cvBossPackEndOfMapVote) && g_bBossPackVoteEnabled && !g_bBossPackVoteCompleted && !g_bBossPackVoteStarted) + { + new startTime = GetConVarInt(g_cvBossPackVoteStartTime) * 60; + if ((time - startTime) <= 0) + { + if (!IsVoteInProgress()) + { + InitiateBossPackVote(); + } + else + { + g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + } + else + { + if (g_hBossPackVoteMapTimer != INVALID_HANDLE) + { + CloseHandle(g_hBossPackVoteMapTimer); + g_hBossPackVoteMapTimer = INVALID_HANDLE; + } + + g_hBossPackVoteMapTimer = CreateTimer(float(time - startTime), Timer_StartBossPackVote, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } +} + +CheckRoundLimitForBossPackVote(roundCount) +{ + if (!GetConVarBool(g_cvBossPackEndOfMapVote) || !g_bBossPackVoteEnabled || g_bBossPackVoteStarted || g_bBossPackVoteCompleted) return; + + if (g_cvMaxRounds == INVALID_HANDLE) return; + + if (GetConVarInt(g_cvMaxRounds) > 0) + { + if (roundCount >= (GetConVarInt(g_cvMaxRounds) - GetConVarInt(g_cvBossPackVoteStartRound))) + { + if (!IsVoteInProgress()) + { + InitiateBossPackVote(); + } + else + { + g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + } + } + + CloseHandle(g_cvMaxRounds); +} + +InitiateBossPackVote() +{ + if (g_bBossPackVoteStarted || g_bBossPackVoteCompleted || IsVoteInProgress()) return; + + // Gather boss packs, if any. + if (g_hBossPackConfig == INVALID_HANDLE) return; + + KvRewind(g_hBossPackConfig); + if (!KvJumpToKey(g_hBossPackConfig, "packs")) return; + if (!KvGotoFirstSubKey(g_hBossPackConfig)) return; + + new Handle:voteMenu = CreateMenu(Menu_BossPackVote); + SetMenuTitle(voteMenu, "%t%t\n \n", "SF2 Prefix", "SF2 Boss Pack Vote Menu Title"); + SetMenuExitBackButton(voteMenu, false); + SetMenuExitButton(voteMenu, false); + + new Handle:menuDisplayNamesTrie = CreateTrie(); + new Handle:menuOptionsInfo = CreateArray(128); + + do + { + if (!bool:KvGetNum(g_hBossPackConfig, "autoload") && bool:KvGetNum(g_hBossPackConfig, "show_in_vote", 1)) + { + decl String:bossPack[128]; + KvGetSectionName(g_hBossPackConfig, bossPack, sizeof(bossPack)); + + decl String:bossPackName[64]; + KvGetString(g_hBossPackConfig, "name", bossPackName, sizeof(bossPackName), bossPack); + + SetTrieString(menuDisplayNamesTrie, bossPack, bossPackName); + PushArrayString(menuOptionsInfo, bossPack); + } + } + while (KvGotoNextKey(g_hBossPackConfig)); + + if (GetArraySize(menuOptionsInfo) == 0) + { + CloseHandle(menuDisplayNamesTrie); + CloseHandle(menuOptionsInfo); + CloseHandle(voteMenu); + return; + } + + if (GetConVarBool(g_cvBossPackVoteShuffle)) + { + SortADTArray(menuOptionsInfo, Sort_Random, Sort_String); + } + + for (new i = 0; i < GetArraySize(menuOptionsInfo); i++) + { + decl String:bossPack[128], String:bossPackName[64]; + GetArrayString(menuOptionsInfo, i, bossPack, sizeof(bossPack)); + GetTrieString(menuDisplayNamesTrie, bossPack, bossPackName, sizeof(bossPackName)); + + AddMenuItem(voteMenu, bossPack, bossPackName); + } + + CloseHandle(menuDisplayNamesTrie); + CloseHandle(menuOptionsInfo); + + g_bBossPackVoteStarted = true; + if (g_hBossPackVoteMapTimer != INVALID_HANDLE) + { + CloseHandle(g_hBossPackVoteMapTimer); + g_hBossPackVoteMapTimer = INVALID_HANDLE; + } + + if (g_hBossPackVoteTimer != INVALID_HANDLE) + { + CloseHandle(g_hBossPackVoteTimer); + g_hBossPackVoteTimer = INVALID_HANDLE; + } + + VoteMenuToAll(voteMenu, 20); +} + +public Menu_BossPackVote(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_VoteStart: + { + g_bBossPackVoteStarted = true; + } + case MenuAction_VoteEnd: + { + g_bBossPackVoteCompleted = true; + + decl String:bossPack[128], String:bossPackName[64]; + GetMenuItem(menu, param1, bossPack, sizeof(bossPack), _, bossPackName, sizeof(bossPackName)); + + SetConVarString(g_cvBossProfilePack, bossPack); + + CPrintToChatAll("%t%t", "SF2 Prefix", "SF2 Boss Pack Vote Successful", bossPackName); + } + case MenuAction_End: + { + g_bBossPackVoteStarted = false; + CloseHandle(menu); + } + } +} + +public Action:Timer_StartBossPackVote(Handle:timer) +{ + if (timer != g_hBossPackVoteMapTimer) return; + + g_hBossPackVoteTimer = CreateTimer(5.0, Timer_BossPackVoteLoop, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hBossPackVoteTimer, true); +} + +public Action:Timer_BossPackVoteLoop(Handle:timer) +{ + if (timer != g_hBossPackVoteTimer || g_bBossPackVoteCompleted || g_bBossPackVoteStarted) return Plugin_Stop; + + if (!IsVoteInProgress()) + { + g_hBossPackVoteTimer = INVALID_HANDLE; + InitiateBossPackVote(); + return Plugin_Stop; + } + + return Plugin_Continue; +} + +bool:IsProfileValid(const String:sProfile[]) +{ + return bool:(FindStringInArray(GetBossProfileList(), sProfile) != -1); +} + +stock GetProfileNum(const String:sProfile[], const String:keyValue[], defaultValue=0) +{ + if (!IsProfileValid(sProfile)) return defaultValue; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + return KvGetNum(g_hConfig, keyValue, defaultValue); +} + +stock Float:GetProfileFloat(const String:sProfile[], const String:keyValue[], Float:defaultValue=0.0) +{ + if (!IsProfileValid(sProfile)) return defaultValue; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + return KvGetFloat(g_hConfig, keyValue, defaultValue); +} + +stock bool:GetProfileVector(const String:sProfile[], const String:keyValue[], Float:buffer[3], const Float:defaultValue[3]=NULL_VECTOR) +{ + for (new i = 0; i < 3; i++) buffer[i] = defaultValue[i]; + + if (!IsProfileValid(sProfile)) return false; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + KvGetVector(g_hConfig, keyValue, buffer, defaultValue); + return true; +} + +stock bool:GetProfileColor(const String:sProfile[], + const String:keyValue[], + &r, + &g, + &b, + &a, + dr=255, + dg=255, + db=255, + da=255) +{ + r = dr; + g = dg; + b = db; + a = da; + + if (!IsProfileValid(sProfile)) return false; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + decl String:sValue[64]; + KvGetString(g_hConfig, keyValue, sValue, sizeof(sValue)); + + if (strlen(sValue) != 0) + { + KvGetColor(g_hConfig, keyValue, r, g, b, a); + } + + return true; +} + +stock bool:GetProfileString(const String:sProfile[], const String:keyValue[], String:buffer[], bufferlen, const String:defaultValue[]="") +{ + strcopy(buffer, bufferlen, defaultValue); + + if (!IsProfileValid(sProfile)) return false; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + KvGetString(g_hConfig, keyValue, buffer, bufferlen, defaultValue); + return true; +} + +GetBossProfileIndexFromName(const String:sProfile[]) +{ + new iReturn = -1; + GetTrieValue(g_hBossProfileNames, sProfile, iReturn); + return iReturn; +} + +GetBossProfileUniqueProfileIndex(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_UniqueProfileIndex); +} + +GetBossProfileSkin(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Skin); +} + +GetBossProfileBodyGroups(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Body); +} + +Float:GetBossProfileModelScale(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ModelScale); +} + +GetBossProfileType(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Type); +} + +GetBossProfileFlags(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_Flags); +} + +Float:GetBossProfileSpeed(iProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedEasy); + case Difficulty_Hard: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedHard); + case Difficulty_Insane: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedInsane); + } + + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SpeedNormal); +} + +Float:GetBossProfileMaxSpeed(iProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedEasy); + case Difficulty_Hard: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedHard); + case Difficulty_Insane: return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedInsane); + } + + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_MaxSpeedNormal); +} + +Float:GetBossProfileSearchRadius(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_SearchRange); +} + +Float:GetBossProfileFOV(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_FieldOfView); +} + +Float:GetBossProfileTurnRate(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_TurnRate); +} + +GetBossProfileEyePositionOffset(iProfileIndex, Float:buffer[3]) +{ + buffer[0] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetX); + buffer[1] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetY); + buffer[2] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyePosOffsetZ); +} + +GetBossProfileEyeAngleOffset(iProfileIndex, Float:buffer[3]) +{ + buffer[0] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetX); + buffer[1] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetY); + buffer[2] = Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_EyeAngOffsetZ); +} + +Float:GetBossProfileAngerStart(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerStart); +} + +Float:GetBossProfileAngerAddOnPageGrab(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerAddOnPageGrab); +} + +Float:GetBossProfileAngerPageGrabTimeDiff(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_AngerPageGrabTimeDiffReq); +} + +Float:GetBossProfileInstantKillRadius(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_InstantKillRadius); +} + +Float:GetBossProfileScareRadius(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ScareRadius); +} + +Float:GetBossProfileScareCooldown(iProfileIndex) +{ + return Float:GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_ScareCooldown); +} + +GetBossProfileTeleportType(iProfileIndex) +{ + return GetArrayCell(g_hBossProfileData, iProfileIndex, BossProfileData_TeleportType); +} + +// Code originally from FF2. Credits to the original authors Rainbolt Dash and FlaminSarge. +stock bool:GetRandomStringFromProfile(const String:sProfile[], const String:strKeyValue[], String:buffer[], bufferlen, index=-1) +{ + strcopy(buffer, bufferlen, ""); + + if (!IsProfileValid(sProfile)) return false; + + KvRewind(g_hConfig); + KvJumpToKey(g_hConfig, sProfile); + + if (!KvJumpToKey(g_hConfig, strKeyValue)) return false; + + decl String:s[32], String:s2[PLATFORM_MAX_PATH]; + + new i = 1; + for (;;) + { + IntToString(i, s, sizeof(s)); + KvGetString(g_hConfig, s, s2, sizeof(s2)); + if (!s2[0]) break; + + i++; + } + + if (i == 1) return false; + + IntToString(index < 0 ? GetRandomInt(1, i - 1) : index, s, sizeof(s)); + KvGetString(g_hConfig, s, buffer, bufferlen); + return true; +} + +/** + * Returns an array of strings of the profile names of every valid boss. + */ +Handle:GetBossProfileList() +{ + return g_hBossProfileList; +} + +/** + * Returns an array of strings of the profile names of every valid boss that can be randomly selected. + */ +Handle:GetSelectableBossProfileList() +{ + return g_hSelectableBossProfileList; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/profiles/profile_chaser.sp b/addons/sourcemod/scripting/rytp_horror/profiles/profile_chaser.sp index 773bc16..e939dfe 100644 --- a/addons/sourcemod/scripting/rytp_horror/profiles/profile_chaser.sp +++ b/addons/sourcemod/scripting/rytp_horror/profiles/profile_chaser.sp @@ -1,571 +1,571 @@ -#if defined _sf2_profiles_chaser - #endinput -#endif - -#define _sf2_profiles_chaser - -#define SF2_CHASER_BOSS_MAX_ATTACKS 8 - -new Handle:g_hChaserProfileNames; -new Handle:g_hChaserProfileData; - -enum -{ - SF2BossAttackType_Invalid = -1, - SF2BossAttackType_Melee = 0, - SF2BossAttackType_Ranged, - SF2BossAttackType_Projectile, - SF2BossAttackType_Custom -}; - -enum -{ - ChaserProfileData_StepSize, - ChaserProfileData_WalkSpeedEasy, - ChaserProfileData_WalkSpeedNormal, - ChaserProfileData_WalkSpeedHard, - ChaserProfileData_WalkSpeedInsane, - - ChaserProfileData_AirSpeedEasy, - ChaserProfileData_AirSpeedNormal, - ChaserProfileData_AirSpeedHard, - ChaserProfileData_AirSpeedInsane, - - ChaserProfileData_MaxWalkSpeedEasy, - ChaserProfileData_MaxWalkSpeedNormal, - ChaserProfileData_MaxWalkSpeedHard, - ChaserProfileData_MaxWalkSpeedInsane, - - ChaserProfileData_MaxAirSpeedEasy, - ChaserProfileData_MaxAirSpeedNormal, - ChaserProfileData_MaxAirSpeedHard, - ChaserProfileData_MaxAirSpeedInsane, - - ChaserProfileData_WakeRadius, - - ChaserProfileData_Attacks, // array that contains data about attacks - - ChaserProfileData_Animations, // Array that contains data of animations. - - ChaserProfileData_CanBeStunned, - ChaserProfileData_StunDuration, - ChaserProfileData_StunHealth, - ChaserProfileData_CanBeStunnedByFlashlight, - ChaserProfileData_StunFlashlightDamage, - - ChaserProfileData_MemoryLifeTime, - - ChaserProfileData_AwarenessIncreaseRateEasy, - ChaserProfileData_AwarenessIncreaseRateNormal, - ChaserProfileData_AwarenessIncreaseRateHard, - ChaserProfileData_AwarenessIncreaseRateInsane, - ChaserProfileData_AwarenessDecreaseRateEasy, - ChaserProfileData_AwarenessDecreaseRateNormal, - ChaserProfileData_AwarenessDecreaseRateHard, - ChaserProfileData_AwarenessDecreaseRateInsane, - - ChaserProfileData_MaxStats -}; - -enum -{ - ChaserProfileAttackData_Type = 0, - ChaserProfileAttackData_CanUseAgainstProps, - ChaserProfileAttackData_Damage, - ChaserProfileAttackData_DamageVsProps, - ChaserProfileAttackData_DamageForce, - ChaserProfileAttackData_DamageType, - ChaserProfileAttackData_DamageDelay, - ChaserProfileAttackData_Range, - ChaserProfileAttackData_Duration, - ChaserProfileAttackData_Spread, - ChaserProfileAttackData_BeginRange, - ChaserProfileAttackData_BeginFOV, - ChaserProfileAttackData_Cooldown, - ChaserProfileAttackData_MaxStats -}; - -enum -{ - ChaserAnimationType_Idle = 0, - ChaserAnimationType_IdlePlaybackRate, - ChaserAnimationType_Walk, - ChaserAnimationType_WalkPlaybackRate, - ChaserAnimationType_Run, - ChaserAnimationType_RunPlaybackRate, - ChaserAnimationType_Attack, - ChaserAnimationType_AttackPlaybackRate, - ChaserAnimationType_Stunned, - ChaserAnimationType_StunnedPlaybackRate, - ChaserAnimationType_Death, - ChaserAnimationType_DeathPlaybackRate, - ChaserAnimationType_Max -}; - -InitializeChaserProfiles() -{ - g_hChaserProfileNames = CreateTrie(); - g_hChaserProfileData = CreateArray(ChaserProfileData_MaxStats); -} - -/** - * Clears all data and memory currently in use by chaser profiles. - */ -ClearChaserProfiles() -{ - for (new i = 0, iSize = GetArraySize(g_hChaserProfileData); i < iSize; i++) - { - new Handle:hHandle = Handle:GetArrayCell(g_hChaserProfileData, i, ChaserProfileData_Attacks); - if (hHandle != INVALID_HANDLE) - { - CloseHandle(hHandle); - } - - hHandle = Handle:GetArrayCell(g_hChaserProfileData, i, ChaserProfileData_Animations); - if (hHandle != INVALID_HANDLE) - { - CloseHandle(hHandle); - } - } - - ClearTrie(g_hChaserProfileNames); - ClearArray(g_hChaserProfileData); -} - -/** - * Parses and stores the unique values of a chaser profile from the current position in the profiles config. - * Returns true if loading was successful, false if not. - */ -bool:LoadChaserBossProfile(Handle:kv, const String:sProfile[], &iUniqueProfileIndex, String:sLoadFailReasonBuffer[], iLoadFailReasonBufferLen) -{ - strcopy(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, ""); - - iUniqueProfileIndex = PushArrayCell(g_hChaserProfileData, -1); - SetTrieValue(g_hChaserProfileNames, sProfile, iUniqueProfileIndex); - - new Float:flBossStepSize = KvGetFloat(kv, "stepsize", 16.0); - - new Float:flBossDefaultWalkSpeed = KvGetFloat(kv, "walkspeed", 30.0); - new Float:flBossWalkSpeedEasy = KvGetFloat(kv, "walkspeed_easy", flBossDefaultWalkSpeed); - new Float:flBossWalkSpeedHard = KvGetFloat(kv, "walkspeed_hard", flBossDefaultWalkSpeed); - new Float:flBossWalkSpeedInsane = KvGetFloat(kv, "walkspeed_insane", flBossDefaultWalkSpeed); - - new Float:flBossDefaultAirSpeed = KvGetFloat(kv, "airspeed", 50.0); - new Float:flBossAirSpeedEasy = KvGetFloat(kv, "airspeed_easy", flBossDefaultAirSpeed); - new Float:flBossAirSpeedHard = KvGetFloat(kv, "airspeed_hard", flBossDefaultAirSpeed); - new Float:flBossAirSpeedInsane = KvGetFloat(kv, "airspeed_insane", flBossDefaultAirSpeed); - - new Float:flBossDefaultMaxWalkSpeed = KvGetFloat(kv, "walkspeed_max", 30.0); - new Float:flBossMaxWalkSpeedEasy = KvGetFloat(kv, "walkspeed_max_easy", flBossDefaultMaxWalkSpeed); - new Float:flBossMaxWalkSpeedHard = KvGetFloat(kv, "walkspeed_max_hard", flBossDefaultMaxWalkSpeed); - new Float:flBossMaxWalkSpeedInsane = KvGetFloat(kv, "walkspeed_max_insane", flBossDefaultMaxWalkSpeed); - - new Float:flBossDefaultMaxAirSpeed = KvGetFloat(kv, "airspeed_max", 50.0); - new Float:flBossMaxAirSpeedEasy = KvGetFloat(kv, "airspeed_max_easy", flBossDefaultMaxAirSpeed); - new Float:flBossMaxAirSpeedHard = KvGetFloat(kv, "airspeed_max_hard", flBossDefaultMaxAirSpeed); - new Float:flBossMaxAirSpeedInsane = KvGetFloat(kv, "airspeed_max_insane", flBossDefaultMaxAirSpeed); - - new Float:flWakeRange = KvGetFloat(kv, "wake_radius"); - if (flWakeRange < 0.0) flWakeRange = 0.0; - - new bool:bCanBeStunned = bool:KvGetNum(kv, "stun_enabled"); - - new Float:flStunDuration = KvGetFloat(kv, "stun_duration"); - if (flStunDuration < 0.0) flStunDuration = 0.0; - - new Float:flStunHealth = KvGetFloat(kv, "stun_health"); - if (flStunHealth < 0.0) flStunHealth = 0.0; - - new bool:bStunTakeDamageFromFlashlight = bool:KvGetNum(kv, "stun_damage_flashlight_enabled"); - - new Float:flStunFlashlightDamage = KvGetFloat(kv, "stun_damage_flashlight"); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossStepSize, ChaserProfileData_StepSize); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultWalkSpeed, ChaserProfileData_WalkSpeedNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedEasy, ChaserProfileData_WalkSpeedEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedHard, ChaserProfileData_WalkSpeedHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedInsane, ChaserProfileData_WalkSpeedInsane); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultAirSpeed, ChaserProfileData_AirSpeedNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedEasy, ChaserProfileData_AirSpeedEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedHard, ChaserProfileData_AirSpeedHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedInsane, ChaserProfileData_AirSpeedInsane); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultMaxWalkSpeed, ChaserProfileData_MaxWalkSpeedNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedEasy, ChaserProfileData_MaxWalkSpeedEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedHard, ChaserProfileData_MaxWalkSpeedHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedInsane, ChaserProfileData_MaxWalkSpeedInsane); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultMaxAirSpeed, ChaserProfileData_MaxAirSpeedNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedEasy, ChaserProfileData_MaxAirSpeedEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedHard, ChaserProfileData_MaxAirSpeedHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedInsane, ChaserProfileData_MaxAirSpeedInsane); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flWakeRange, ChaserProfileData_WakeRadius); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, bCanBeStunned, ChaserProfileData_CanBeStunned); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunDuration, ChaserProfileData_StunDuration); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunHealth, ChaserProfileData_StunHealth); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, bStunTakeDamageFromFlashlight, ChaserProfileData_CanBeStunnedByFlashlight); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunFlashlightDamage, ChaserProfileData_StunFlashlightDamage); - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "memory_lifetime", 10.0), ChaserProfileData_MemoryLifeTime); - - new Float:flDefaultAwarenessIncreaseRate = KvGetFloat(kv, "awareness_rate_increase", 75.0); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_easy", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flDefaultAwarenessIncreaseRate, ChaserProfileData_AwarenessIncreaseRateNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_hard", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_insane", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateInsane); - - new Float:flDefaultAwarenessDecreaseRate = KvGetFloat(kv, "awareness_rate_decrease", 150.0); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_easy", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateEasy); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flDefaultAwarenessDecreaseRate, ChaserProfileData_AwarenessDecreaseRateNormal); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_hard", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateHard); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_insane", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateInsane); - - ParseChaserProfileAttacks(kv, iUniqueProfileIndex); - - ParseChaserProfileAnimations(kv, iUniqueProfileIndex); - - return true; -} - -static ParseChaserProfileAttacks(Handle:kv, iUniqueProfileIndex) -{ - //decl String:sBuffer[PLATFORM_MAX_PATH]; - - // Create the array. - new Handle:hAttacks = CreateArray(ChaserProfileAttackData_MaxStats); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, hAttacks, ChaserProfileData_Attacks); - - //new iAttackType = KvGetNum(kv, "attack_type"); - new iAttackType = SF2BossAttackType_Melee; - - new Float:flAttackRange = KvGetFloat(kv, "attack_range"); - if (flAttackRange < 0.0) flAttackRange = 0.0; - - new Float:flAttackDamage = KvGetFloat(kv, "attack_damage"); - new Float:flAttackDamageVsProps = KvGetFloat(kv, "attack_damage_vs_props", flAttackDamage); - new Float:flAttackDamageForce = KvGetFloat(kv, "attack_damageforce"); - - new iAttackDamageType = KvGetNum(kv, "attack_damagetype"); - if (iAttackDamageType < 0) iAttackDamageType = 0; - - new Float:flAttackDamageDelay = KvGetFloat(kv, "attack_delay"); - if (flAttackDamageDelay < 0.0) flAttackDamageDelay = 0.0; - - new Float:flAttackDuration = KvGetFloat(kv, "attack_duration"); - if (flAttackDuration < 0.0) flAttackDuration = 0.0; - - new bool:bAttackProps = bool:KvGetNum(kv, "attack_props"); - - new Float:flAttackSpreadOld = KvGetFloat(kv, "attack_fov", 45.0); - new Float:flAttackSpread = KvGetFloat(kv, "attack_spread", flAttackSpreadOld); - - if (flAttackSpread < 0.0) flAttackSpread = 0.0; - else if (flAttackSpread > 360.0) flAttackSpread = 360.0; - - new Float:flAttackBeginRange = KvGetFloat(kv, "attack_begin_range", flAttackRange); - if (flAttackBeginRange < 0.0) flAttackBeginRange = 0.0; - - new Float:flAttackBeginFOV = KvGetFloat(kv, "attack_begin_fov", flAttackSpread); - if (flAttackBeginFOV < 0.0) flAttackBeginFOV = 0.0; - else if (flAttackBeginFOV > 360.0) flAttackBeginFOV = 360.0; - - new Float:flAttackCooldown = KvGetFloat(kv, "attack_cooldown"); - if (flAttackCooldown < 0.0) flAttackCooldown = 0.0; - - new iAttackIndex = PushArrayCell(hAttacks, -1); - - SetArrayCell(hAttacks, iAttackIndex, iAttackType, ChaserProfileAttackData_Type); - SetArrayCell(hAttacks, iAttackIndex, bAttackProps, ChaserProfileAttackData_CanUseAgainstProps); - SetArrayCell(hAttacks, iAttackIndex, flAttackRange, ChaserProfileAttackData_Range); - SetArrayCell(hAttacks, iAttackIndex, flAttackDamage, ChaserProfileAttackData_Damage); - SetArrayCell(hAttacks, iAttackIndex, flAttackDamageVsProps, ChaserProfileAttackData_DamageVsProps); - SetArrayCell(hAttacks, iAttackIndex, flAttackDamageForce, ChaserProfileAttackData_DamageForce); - SetArrayCell(hAttacks, iAttackIndex, iAttackDamageType, ChaserProfileAttackData_DamageType); - SetArrayCell(hAttacks, iAttackIndex, flAttackDamageDelay, ChaserProfileAttackData_DamageDelay); - SetArrayCell(hAttacks, iAttackIndex, flAttackDuration, ChaserProfileAttackData_Duration); - SetArrayCell(hAttacks, iAttackIndex, flAttackSpread, ChaserProfileAttackData_Spread); - SetArrayCell(hAttacks, iAttackIndex, flAttackBeginRange, ChaserProfileAttackData_BeginRange); - SetArrayCell(hAttacks, iAttackIndex, flAttackBeginFOV, ChaserProfileAttackData_BeginFOV); - SetArrayCell(hAttacks, iAttackIndex, flAttackCooldown, ChaserProfileAttackData_Cooldown); -} - -/** - * Parses and stores the default animations of a chaser boss profile. - */ -static ParseChaserProfileAnimations(Handle:kv, iUniqueProfileIndex) -{ - new Handle:hAnimations = CreateArray(64); - for (new i = 0; i < ChaserAnimationType_Max / 2; i++) - { - PushArrayString(hAnimations, ""); - PushArrayCell(hAnimations, 1.0); - } - - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, hAnimations, ChaserProfileData_Animations); - - decl String:sAnimation[64]; - decl Float:flAnimationPlaybackRate; - new animationCount = 0; - - KvGetString(kv, "animation_idle", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_idle_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Idle, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_IdlePlaybackRate, flAnimationPlaybackRate); - } - - KvGetString(kv, "animation_walk", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_walk_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Walk, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_WalkPlaybackRate, flAnimationPlaybackRate); - } - - KvGetString(kv, "animation_run", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_run_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Run, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_RunPlaybackRate, flAnimationPlaybackRate); - } - - KvGetString(kv, "animation_attack", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_attack_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Attack, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_AttackPlaybackRate, flAnimationPlaybackRate); - } - - KvGetString(kv, "animation_stun", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_stun_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Stunned, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_StunnedPlaybackRate, flAnimationPlaybackRate); - } - - KvGetString(kv, "animation_death", sAnimation, sizeof(sAnimation)); - flAnimationPlaybackRate = KvGetFloat(kv, "animation_death_playbackrate", 1.0); - if (sAnimation[0]) - { - animationCount++; - SetArrayString(hAnimations, ChaserAnimationType_Stunned, sAnimation); - SetArrayCell(hAnimations, ChaserAnimationType_StunnedPlaybackRate, flAnimationPlaybackRate); - } - - if (animationCount == 0) - { - CloseHandle(hAnimations); - SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, INVALID_HANDLE, ChaserProfileData_Animations); - } -} - -Float:GetChaserProfileStepSize(iChaserProfileIndex) -{ - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StepSize); -} - -Float:GetChaserProfileWalkSpeed(iChaserProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedNormal); -} - -Float:GetChaserProfileAirSpeed(iChaserProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedNormal); -} - -Float:GetChaserProfileMaxWalkSpeed(iChaserProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedNormal); -} - -Float:GetChaserProfileMaxAirSpeed(iChaserProfileIndex, iDifficulty) -{ - switch (iDifficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedNormal); -} - -Float:GetChaserProfileWakeRadius(iChaserProfileIndex) -{ - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WakeRadius); -} - -GetChaserProfileAttackCount(iChaserProfileIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return GetArraySize(hAttacks); -} - -GetChaserProfileAttackType(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Type); -} - -Float:GetChaserProfileAttackDamage(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Damage); -} - -Float:GetChaserProfileAttackDamageVsProps(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageVsProps); -} - -Float:GetChaserProfileAttackDamageForce(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageForce); -} - -GetChaserProfileAttackDamageType(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageType); -} - -Float:GetChaserProfileAttackDamageDelay(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageDelay); -} - -Float:GetChaserProfileAttackRange(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Range); -} - -Float:GetChaserProfileAttackDuration(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Duration); -} - -Float:GetChaserProfileAttackSpread(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Spread); -} - -Float:GetChaserProfileAttackBeginRange(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_BeginRange); -} - -Float:GetChaserProfileAttackBeginFOV(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_BeginFOV); -} - -Float:GetChaserProfileAttackCooldown(iChaserProfileIndex, iAttackIndex) -{ - new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); - - return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Cooldown); -} - -bool:GetChaserProfileStunState(iChaserProfileIndex) -{ - return bool:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_CanBeStunned); -} - -Float:GetChaserProfileStunDuration(iChaserProfileIndex) -{ - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunDuration); -} - -bool:GetChaserProfileStunFlashlightState(iChaserProfileIndex) -{ - return bool:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_CanBeStunnedByFlashlight); -} - -Float:GetChaserProfileStunFlashlightDamage(iChaserProfileIndex) -{ - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunFlashlightDamage); -} - -Float:GetChaserProfileStunHealth(iChaserProfileIndex) -{ - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunHealth); -} - -stock Float:GetChaserProfileAwarenessIncreaseRate(iChaserProfileIndex, difficulty) -{ - switch (difficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateNormal); -} - -stock Float:GetChaserProfileAwarenessDecreaseRate(iChaserProfileIndex, difficulty) -{ - switch (difficulty) - { - case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateEasy); - case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateHard); - case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateInsane); - } - - return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateNormal); +#if defined _sf2_profiles_chaser + #endinput +#endif + +#define _sf2_profiles_chaser + +#define SF2_CHASER_BOSS_MAX_ATTACKS 8 + +new Handle:g_hChaserProfileNames; +new Handle:g_hChaserProfileData; + +enum +{ + SF2BossAttackType_Invalid = -1, + SF2BossAttackType_Melee = 0, + SF2BossAttackType_Ranged, + SF2BossAttackType_Projectile, + SF2BossAttackType_Custom +}; + +enum +{ + ChaserProfileData_StepSize, + ChaserProfileData_WalkSpeedEasy, + ChaserProfileData_WalkSpeedNormal, + ChaserProfileData_WalkSpeedHard, + ChaserProfileData_WalkSpeedInsane, + + ChaserProfileData_AirSpeedEasy, + ChaserProfileData_AirSpeedNormal, + ChaserProfileData_AirSpeedHard, + ChaserProfileData_AirSpeedInsane, + + ChaserProfileData_MaxWalkSpeedEasy, + ChaserProfileData_MaxWalkSpeedNormal, + ChaserProfileData_MaxWalkSpeedHard, + ChaserProfileData_MaxWalkSpeedInsane, + + ChaserProfileData_MaxAirSpeedEasy, + ChaserProfileData_MaxAirSpeedNormal, + ChaserProfileData_MaxAirSpeedHard, + ChaserProfileData_MaxAirSpeedInsane, + + ChaserProfileData_WakeRadius, + + ChaserProfileData_Attacks, // array that contains data about attacks + + ChaserProfileData_Animations, // Array that contains data of animations. + + ChaserProfileData_CanBeStunned, + ChaserProfileData_StunDuration, + ChaserProfileData_StunHealth, + ChaserProfileData_CanBeStunnedByFlashlight, + ChaserProfileData_StunFlashlightDamage, + + ChaserProfileData_MemoryLifeTime, + + ChaserProfileData_AwarenessIncreaseRateEasy, + ChaserProfileData_AwarenessIncreaseRateNormal, + ChaserProfileData_AwarenessIncreaseRateHard, + ChaserProfileData_AwarenessIncreaseRateInsane, + ChaserProfileData_AwarenessDecreaseRateEasy, + ChaserProfileData_AwarenessDecreaseRateNormal, + ChaserProfileData_AwarenessDecreaseRateHard, + ChaserProfileData_AwarenessDecreaseRateInsane, + + ChaserProfileData_MaxStats +}; + +enum +{ + ChaserProfileAttackData_Type = 0, + ChaserProfileAttackData_CanUseAgainstProps, + ChaserProfileAttackData_Damage, + ChaserProfileAttackData_DamageVsProps, + ChaserProfileAttackData_DamageForce, + ChaserProfileAttackData_DamageType, + ChaserProfileAttackData_DamageDelay, + ChaserProfileAttackData_Range, + ChaserProfileAttackData_Duration, + ChaserProfileAttackData_Spread, + ChaserProfileAttackData_BeginRange, + ChaserProfileAttackData_BeginFOV, + ChaserProfileAttackData_Cooldown, + ChaserProfileAttackData_MaxStats +}; + +enum +{ + ChaserAnimationType_Idle = 0, + ChaserAnimationType_IdlePlaybackRate, + ChaserAnimationType_Walk, + ChaserAnimationType_WalkPlaybackRate, + ChaserAnimationType_Run, + ChaserAnimationType_RunPlaybackRate, + ChaserAnimationType_Attack, + ChaserAnimationType_AttackPlaybackRate, + ChaserAnimationType_Stunned, + ChaserAnimationType_StunnedPlaybackRate, + ChaserAnimationType_Death, + ChaserAnimationType_DeathPlaybackRate, + ChaserAnimationType_Max +}; + +InitializeChaserProfiles() +{ + g_hChaserProfileNames = CreateTrie(); + g_hChaserProfileData = CreateArray(ChaserProfileData_MaxStats); +} + +/** + * Clears all data and memory currently in use by chaser profiles. + */ +ClearChaserProfiles() +{ + for (new i = 0, iSize = GetArraySize(g_hChaserProfileData); i < iSize; i++) + { + new Handle:hHandle = Handle:GetArrayCell(g_hChaserProfileData, i, ChaserProfileData_Attacks); + if (hHandle != INVALID_HANDLE) + { + CloseHandle(hHandle); + } + + hHandle = Handle:GetArrayCell(g_hChaserProfileData, i, ChaserProfileData_Animations); + if (hHandle != INVALID_HANDLE) + { + CloseHandle(hHandle); + } + } + + ClearTrie(g_hChaserProfileNames); + ClearArray(g_hChaserProfileData); +} + +/** + * Parses and stores the unique values of a chaser profile from the current position in the profiles config. + * Returns true if loading was successful, false if not. + */ +bool:LoadChaserBossProfile(Handle:kv, const String:sProfile[], &iUniqueProfileIndex, String:sLoadFailReasonBuffer[], iLoadFailReasonBufferLen) +{ + strcopy(sLoadFailReasonBuffer, iLoadFailReasonBufferLen, ""); + + iUniqueProfileIndex = PushArrayCell(g_hChaserProfileData, -1); + SetTrieValue(g_hChaserProfileNames, sProfile, iUniqueProfileIndex); + + new Float:flBossStepSize = KvGetFloat(kv, "stepsize", 16.0); + + new Float:flBossDefaultWalkSpeed = KvGetFloat(kv, "walkspeed", 30.0); + new Float:flBossWalkSpeedEasy = KvGetFloat(kv, "walkspeed_easy", flBossDefaultWalkSpeed); + new Float:flBossWalkSpeedHard = KvGetFloat(kv, "walkspeed_hard", flBossDefaultWalkSpeed); + new Float:flBossWalkSpeedInsane = KvGetFloat(kv, "walkspeed_insane", flBossDefaultWalkSpeed); + + new Float:flBossDefaultAirSpeed = KvGetFloat(kv, "airspeed", 50.0); + new Float:flBossAirSpeedEasy = KvGetFloat(kv, "airspeed_easy", flBossDefaultAirSpeed); + new Float:flBossAirSpeedHard = KvGetFloat(kv, "airspeed_hard", flBossDefaultAirSpeed); + new Float:flBossAirSpeedInsane = KvGetFloat(kv, "airspeed_insane", flBossDefaultAirSpeed); + + new Float:flBossDefaultMaxWalkSpeed = KvGetFloat(kv, "walkspeed_max", 30.0); + new Float:flBossMaxWalkSpeedEasy = KvGetFloat(kv, "walkspeed_max_easy", flBossDefaultMaxWalkSpeed); + new Float:flBossMaxWalkSpeedHard = KvGetFloat(kv, "walkspeed_max_hard", flBossDefaultMaxWalkSpeed); + new Float:flBossMaxWalkSpeedInsane = KvGetFloat(kv, "walkspeed_max_insane", flBossDefaultMaxWalkSpeed); + + new Float:flBossDefaultMaxAirSpeed = KvGetFloat(kv, "airspeed_max", 50.0); + new Float:flBossMaxAirSpeedEasy = KvGetFloat(kv, "airspeed_max_easy", flBossDefaultMaxAirSpeed); + new Float:flBossMaxAirSpeedHard = KvGetFloat(kv, "airspeed_max_hard", flBossDefaultMaxAirSpeed); + new Float:flBossMaxAirSpeedInsane = KvGetFloat(kv, "airspeed_max_insane", flBossDefaultMaxAirSpeed); + + new Float:flWakeRange = KvGetFloat(kv, "wake_radius"); + if (flWakeRange < 0.0) flWakeRange = 0.0; + + new bool:bCanBeStunned = bool:KvGetNum(kv, "stun_enabled"); + + new Float:flStunDuration = KvGetFloat(kv, "stun_duration"); + if (flStunDuration < 0.0) flStunDuration = 0.0; + + new Float:flStunHealth = KvGetFloat(kv, "stun_health"); + if (flStunHealth < 0.0) flStunHealth = 0.0; + + new bool:bStunTakeDamageFromFlashlight = bool:KvGetNum(kv, "stun_damage_flashlight_enabled"); + + new Float:flStunFlashlightDamage = KvGetFloat(kv, "stun_damage_flashlight"); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossStepSize, ChaserProfileData_StepSize); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultWalkSpeed, ChaserProfileData_WalkSpeedNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedEasy, ChaserProfileData_WalkSpeedEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedHard, ChaserProfileData_WalkSpeedHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossWalkSpeedInsane, ChaserProfileData_WalkSpeedInsane); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultAirSpeed, ChaserProfileData_AirSpeedNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedEasy, ChaserProfileData_AirSpeedEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedHard, ChaserProfileData_AirSpeedHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossAirSpeedInsane, ChaserProfileData_AirSpeedInsane); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultMaxWalkSpeed, ChaserProfileData_MaxWalkSpeedNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedEasy, ChaserProfileData_MaxWalkSpeedEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedHard, ChaserProfileData_MaxWalkSpeedHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxWalkSpeedInsane, ChaserProfileData_MaxWalkSpeedInsane); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossDefaultMaxAirSpeed, ChaserProfileData_MaxAirSpeedNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedEasy, ChaserProfileData_MaxAirSpeedEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedHard, ChaserProfileData_MaxAirSpeedHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flBossMaxAirSpeedInsane, ChaserProfileData_MaxAirSpeedInsane); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flWakeRange, ChaserProfileData_WakeRadius); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, bCanBeStunned, ChaserProfileData_CanBeStunned); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunDuration, ChaserProfileData_StunDuration); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunHealth, ChaserProfileData_StunHealth); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, bStunTakeDamageFromFlashlight, ChaserProfileData_CanBeStunnedByFlashlight); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flStunFlashlightDamage, ChaserProfileData_StunFlashlightDamage); + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "memory_lifetime", 10.0), ChaserProfileData_MemoryLifeTime); + + new Float:flDefaultAwarenessIncreaseRate = KvGetFloat(kv, "awareness_rate_increase", 75.0); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_easy", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flDefaultAwarenessIncreaseRate, ChaserProfileData_AwarenessIncreaseRateNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_hard", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_increase_insane", flDefaultAwarenessIncreaseRate), ChaserProfileData_AwarenessIncreaseRateInsane); + + new Float:flDefaultAwarenessDecreaseRate = KvGetFloat(kv, "awareness_rate_decrease", 150.0); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_easy", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateEasy); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, flDefaultAwarenessDecreaseRate, ChaserProfileData_AwarenessDecreaseRateNormal); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_hard", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateHard); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, KvGetFloat(kv, "awareness_rate_decrease_insane", flDefaultAwarenessDecreaseRate), ChaserProfileData_AwarenessDecreaseRateInsane); + + ParseChaserProfileAttacks(kv, iUniqueProfileIndex); + + ParseChaserProfileAnimations(kv, iUniqueProfileIndex); + + return true; +} + +static ParseChaserProfileAttacks(Handle:kv, iUniqueProfileIndex) +{ + //decl String:sBuffer[PLATFORM_MAX_PATH]; + + // Create the array. + new Handle:hAttacks = CreateArray(ChaserProfileAttackData_MaxStats); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, hAttacks, ChaserProfileData_Attacks); + + //new iAttackType = KvGetNum(kv, "attack_type"); + new iAttackType = SF2BossAttackType_Melee; + + new Float:flAttackRange = KvGetFloat(kv, "attack_range"); + if (flAttackRange < 0.0) flAttackRange = 0.0; + + new Float:flAttackDamage = KvGetFloat(kv, "attack_damage"); + new Float:flAttackDamageVsProps = KvGetFloat(kv, "attack_damage_vs_props", flAttackDamage); + new Float:flAttackDamageForce = KvGetFloat(kv, "attack_damageforce"); + + new iAttackDamageType = KvGetNum(kv, "attack_damagetype"); + if (iAttackDamageType < 0) iAttackDamageType = 0; + + new Float:flAttackDamageDelay = KvGetFloat(kv, "attack_delay"); + if (flAttackDamageDelay < 0.0) flAttackDamageDelay = 0.0; + + new Float:flAttackDuration = KvGetFloat(kv, "attack_duration"); + if (flAttackDuration < 0.0) flAttackDuration = 0.0; + + new bool:bAttackProps = bool:KvGetNum(kv, "attack_props"); + + new Float:flAttackSpreadOld = KvGetFloat(kv, "attack_fov", 45.0); + new Float:flAttackSpread = KvGetFloat(kv, "attack_spread", flAttackSpreadOld); + + if (flAttackSpread < 0.0) flAttackSpread = 0.0; + else if (flAttackSpread > 360.0) flAttackSpread = 360.0; + + new Float:flAttackBeginRange = KvGetFloat(kv, "attack_begin_range", flAttackRange); + if (flAttackBeginRange < 0.0) flAttackBeginRange = 0.0; + + new Float:flAttackBeginFOV = KvGetFloat(kv, "attack_begin_fov", flAttackSpread); + if (flAttackBeginFOV < 0.0) flAttackBeginFOV = 0.0; + else if (flAttackBeginFOV > 360.0) flAttackBeginFOV = 360.0; + + new Float:flAttackCooldown = KvGetFloat(kv, "attack_cooldown"); + if (flAttackCooldown < 0.0) flAttackCooldown = 0.0; + + new iAttackIndex = PushArrayCell(hAttacks, -1); + + SetArrayCell(hAttacks, iAttackIndex, iAttackType, ChaserProfileAttackData_Type); + SetArrayCell(hAttacks, iAttackIndex, bAttackProps, ChaserProfileAttackData_CanUseAgainstProps); + SetArrayCell(hAttacks, iAttackIndex, flAttackRange, ChaserProfileAttackData_Range); + SetArrayCell(hAttacks, iAttackIndex, flAttackDamage, ChaserProfileAttackData_Damage); + SetArrayCell(hAttacks, iAttackIndex, flAttackDamageVsProps, ChaserProfileAttackData_DamageVsProps); + SetArrayCell(hAttacks, iAttackIndex, flAttackDamageForce, ChaserProfileAttackData_DamageForce); + SetArrayCell(hAttacks, iAttackIndex, iAttackDamageType, ChaserProfileAttackData_DamageType); + SetArrayCell(hAttacks, iAttackIndex, flAttackDamageDelay, ChaserProfileAttackData_DamageDelay); + SetArrayCell(hAttacks, iAttackIndex, flAttackDuration, ChaserProfileAttackData_Duration); + SetArrayCell(hAttacks, iAttackIndex, flAttackSpread, ChaserProfileAttackData_Spread); + SetArrayCell(hAttacks, iAttackIndex, flAttackBeginRange, ChaserProfileAttackData_BeginRange); + SetArrayCell(hAttacks, iAttackIndex, flAttackBeginFOV, ChaserProfileAttackData_BeginFOV); + SetArrayCell(hAttacks, iAttackIndex, flAttackCooldown, ChaserProfileAttackData_Cooldown); +} + +/** + * Parses and stores the default animations of a chaser boss profile. + */ +static ParseChaserProfileAnimations(Handle:kv, iUniqueProfileIndex) +{ + new Handle:hAnimations = CreateArray(64); + for (new i = 0; i < ChaserAnimationType_Max / 2; i++) + { + PushArrayString(hAnimations, ""); + PushArrayCell(hAnimations, 1.0); + } + + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, hAnimations, ChaserProfileData_Animations); + + decl String:sAnimation[64]; + decl Float:flAnimationPlaybackRate; + new animationCount = 0; + + KvGetString(kv, "animation_idle", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_idle_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Idle, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_IdlePlaybackRate, flAnimationPlaybackRate); + } + + KvGetString(kv, "animation_walk", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_walk_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Walk, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_WalkPlaybackRate, flAnimationPlaybackRate); + } + + KvGetString(kv, "animation_run", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_run_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Run, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_RunPlaybackRate, flAnimationPlaybackRate); + } + + KvGetString(kv, "animation_attack", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_attack_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Attack, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_AttackPlaybackRate, flAnimationPlaybackRate); + } + + KvGetString(kv, "animation_stun", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_stun_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Stunned, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_StunnedPlaybackRate, flAnimationPlaybackRate); + } + + KvGetString(kv, "animation_death", sAnimation, sizeof(sAnimation)); + flAnimationPlaybackRate = KvGetFloat(kv, "animation_death_playbackrate", 1.0); + if (sAnimation[0]) + { + animationCount++; + SetArrayString(hAnimations, ChaserAnimationType_Stunned, sAnimation); + SetArrayCell(hAnimations, ChaserAnimationType_StunnedPlaybackRate, flAnimationPlaybackRate); + } + + if (animationCount == 0) + { + CloseHandle(hAnimations); + SetArrayCell(g_hChaserProfileData, iUniqueProfileIndex, INVALID_HANDLE, ChaserProfileData_Animations); + } +} + +Float:GetChaserProfileStepSize(iChaserProfileIndex) +{ + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StepSize); +} + +Float:GetChaserProfileWalkSpeed(iChaserProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WalkSpeedNormal); +} + +Float:GetChaserProfileAirSpeed(iChaserProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AirSpeedNormal); +} + +Float:GetChaserProfileMaxWalkSpeed(iChaserProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxWalkSpeedNormal); +} + +Float:GetChaserProfileMaxAirSpeed(iChaserProfileIndex, iDifficulty) +{ + switch (iDifficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_MaxAirSpeedNormal); +} + +Float:GetChaserProfileWakeRadius(iChaserProfileIndex) +{ + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_WakeRadius); +} + +GetChaserProfileAttackCount(iChaserProfileIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return GetArraySize(hAttacks); +} + +GetChaserProfileAttackType(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Type); +} + +Float:GetChaserProfileAttackDamage(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Damage); +} + +Float:GetChaserProfileAttackDamageVsProps(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageVsProps); +} + +Float:GetChaserProfileAttackDamageForce(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageForce); +} + +GetChaserProfileAttackDamageType(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageType); +} + +Float:GetChaserProfileAttackDamageDelay(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_DamageDelay); +} + +Float:GetChaserProfileAttackRange(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Range); +} + +Float:GetChaserProfileAttackDuration(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Duration); +} + +Float:GetChaserProfileAttackSpread(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Spread); +} + +Float:GetChaserProfileAttackBeginRange(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_BeginRange); +} + +Float:GetChaserProfileAttackBeginFOV(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_BeginFOV); +} + +Float:GetChaserProfileAttackCooldown(iChaserProfileIndex, iAttackIndex) +{ + new Handle:hAttacks = Handle:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_Attacks); + + return Float:GetArrayCell(hAttacks, iAttackIndex, ChaserProfileAttackData_Cooldown); +} + +bool:GetChaserProfileStunState(iChaserProfileIndex) +{ + return bool:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_CanBeStunned); +} + +Float:GetChaserProfileStunDuration(iChaserProfileIndex) +{ + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunDuration); +} + +bool:GetChaserProfileStunFlashlightState(iChaserProfileIndex) +{ + return bool:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_CanBeStunnedByFlashlight); +} + +Float:GetChaserProfileStunFlashlightDamage(iChaserProfileIndex) +{ + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunFlashlightDamage); +} + +Float:GetChaserProfileStunHealth(iChaserProfileIndex) +{ + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_StunHealth); +} + +stock Float:GetChaserProfileAwarenessIncreaseRate(iChaserProfileIndex, difficulty) +{ + switch (difficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessIncreaseRateNormal); +} + +stock Float:GetChaserProfileAwarenessDecreaseRate(iChaserProfileIndex, difficulty) +{ + switch (difficulty) + { + case Difficulty_Easy: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateEasy); + case Difficulty_Hard: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateHard); + case Difficulty_Insane: return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateInsane); + } + + return Float:GetArrayCell(g_hChaserProfileData, iChaserProfileIndex, ChaserProfileData_AwarenessDecreaseRateNormal); } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/pvp.sp b/addons/sourcemod/scripting/rytp_horror/pvp.sp index f15ccd5..1cfc17f 100644 --- a/addons/sourcemod/scripting/rytp_horror/pvp.sp +++ b/addons/sourcemod/scripting/rytp_horror/pvp.sp @@ -1,582 +1,582 @@ -#if defined _sf2_pvp_included - #endinput -#endif -#define _sf2_pvp_included - - -#define SF2_PVP_SPAWN_SOUND "items/spawn_item.wav" - -new Handle:g_cvPvPArenaLeaveTime; -new Handle:g_cvPvPArenaPlayerCollisions; - -static const String:g_sPvPProjectileClasses[][] = -{ - "tf_projectile_rocket", - "tf_projectile_sentryrocket", - "tf_projectile_arrow", - "tf_projectile_stun_ball", - "tf_projectile_ball_ornament", - "tf_projectile_cleaver", - "tf_projectile_energy_ball", - "tf_projectile_energy_ring", - "tf_projectile_flare", - "tf_projectile_healing_bolt", - "tf_projectile_jar", - "tf_projectile_jar_milk", - "tf_projectile_pipe", - "tf_projectile_pipe_remote", - "tf_projectile_syringe" -}; - -static bool:g_bPlayerInPvP[MAXPLAYERS + 1]; -static Handle:g_hPlayerPvPTimer[MAXPLAYERS + 1]; -static Handle:g_hPlayerPvPRespawnTimer[MAXPLAYERS + 1]; -static g_iPlayerPvPTimerCount[MAXPLAYERS + 1]; -static bool:g_bPlayerInPvPTrigger[MAXPLAYERS + 1]; - -static Handle:g_hPvPFlameEntities; - -enum -{ - PvPFlameEntData_EntRef = 0, - PvPFlameEntData_LastHitEntRef, - PvPFlameEntData_MaxStats -}; - -public PvP_Initialize() -{ - g_cvPvPArenaLeaveTime = CreateConVar("sf2_player_pvparena_leavetime", "3"); - g_cvPvPArenaPlayerCollisions = CreateConVar("sf2_player_pvparena_collisions", "1"); - - g_hPvPFlameEntities = CreateArray(PvPFlameEntData_MaxStats); -} - -public PvP_SetupMenus() -{ - g_hMenuSettingsPvP = CreateMenu(Menu_SettingsPvP); - SetMenuTitle(g_hMenuSettingsPvP, "%t%t\n \n", "SF2 Prefix", "SF2 Settings PvP Menu Title"); - AddMenuItem(g_hMenuSettingsPvP, "0", "Toggle automatic spawning"); - SetMenuExitBackButton(g_hMenuSettingsPvP, true); -} - -public PvP_OnMapStart() -{ - ClearArray(g_hPvPFlameEntities); -} - -public PvP_Precache() -{ - PrecacheSound2(SF2_PVP_SPAWN_SOUND); -} - -public PvP_OnClientPutInServer(client) -{ - PvP_ForceResetPlayerPvPData(client); -} - -public PvP_OnClientDisconnect(client) -{ - PvP_SetPlayerPvPState(client, false, false, false); -} - -public PvP_OnGameFrame() -{ - // Process through PvP projectiles. - for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) - { - new ent = -1; - while ((ent = FindEntityByClassname(ent, g_sPvPProjectileClasses[i])) != -1) - { - new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); - new bool:bChangeProjectileTeam = false; - - new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); - if (IsValidClient(iOwnerEntity) && IsClientInPvP(iOwnerEntity)) - { - bChangeProjectileTeam = true; - } - else if (iThrowerOffset != -1) - { - iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); - if (IsValidClient(iOwnerEntity) && IsClientInPvP(iOwnerEntity)) - { - bChangeProjectileTeam = true; - } - } - - if (bChangeProjectileTeam) - { - SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); - SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); - } - } - } - - // Process through PvP flame entities. - { - static Float:flMins[3] = { -6.0, ... }; - static Float:flMaxs[3] = { 6.0, ... }; - - decl Float:flOrigin[3]; - - new Handle:hTrace = INVALID_HANDLE; - new ent = -1; - new iOwnerEntity = INVALID_ENT_REFERENCE; - new iHitEntity = INVALID_ENT_REFERENCE; - - while ((ent = FindEntityByClassname(ent, "tf_flame")) != -1) - { - iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); - - if (IsValidEdict(iOwnerEntity)) - { - // tf_flame's initial owner SHOULD be the flamethrower that it originates from. - // If not, then something's completely bogus. - - iOwnerEntity = GetEntPropEnt(iOwnerEntity, Prop_Data, "m_hOwnerEntity"); - } - - if (IsValidClient(iOwnerEntity) && (IsRoundInWarmup() || IsClientInPvP(iOwnerEntity))) - { - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flOrigin); - - hTrace = TR_TraceHullFilterEx(flOrigin, flOrigin, flMins, flMaxs, MASK_PLAYERSOLID, TraceRayDontHitEntity, iOwnerEntity); - iHitEntity = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (IsValidEntity(iHitEntity)) - { - new entref = EntIndexToEntRef(ent); - - new iIndex = FindValueInArray(g_hPvPFlameEntities, entref); - if (iIndex != -1) - { - new iLastHitEnt = EntRefToEntIndex(GetArrayCell(g_hPvPFlameEntities, iIndex, PvPFlameEntData_LastHitEntRef)); - - if (iHitEntity != iLastHitEnt) - { - SetArrayCell(g_hPvPFlameEntities, iIndex, EntIndexToEntRef(iHitEntity), PvPFlameEntData_LastHitEntRef); - PvP_OnFlameEntityStartTouchPost(ent, iHitEntity); - } - } - } - } - } - } -} - -public PvP_OnEntityCreated(ent, const String:sClassname[]) -{ - if (StrEqual(sClassname, "tf_flame", false)) - { - new iIndex = PushArrayCell(g_hPvPFlameEntities, EntIndexToEntRef(ent)); - if (iIndex != -1) - { - SetArrayCell(g_hPvPFlameEntities, iIndex, INVALID_ENT_REFERENCE, PvPFlameEntData_LastHitEntRef); - } - } - else - { - for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) - { - if (StrEqual(sClassname, g_sPvPProjectileClasses[i], false)) - { - SDKHook(ent, SDKHook_Spawn, Hook_PvPProjectileSpawn); - SDKHook(ent, SDKHook_SpawnPost, Hook_PvPProjectileSpawnPost); - break; - } - } - } -} - -public PvP_OnEntityDestroyed(ent, const String:sClassname[]) -{ - if (StrEqual(sClassname, "tf_flame", false)) - { - new entref = EntIndexToEntRef(ent); - new iIndex = FindValueInArray(g_hPvPFlameEntities, entref); - if (iIndex != -1) - { - RemoveFromArray(g_hPvPFlameEntities, iIndex); - } - } -} - -public Action:Hook_PvPProjectileSpawn(ent) -{ - decl String:sClass[64]; - GetEntityClassname(ent, sClass, sizeof(sClass)); - - new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); - new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); - - if (iOwnerEntity == -1 && iThrowerOffset != -1) - { - iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); - } - - if (IsValidClient(iOwnerEntity)) - { - if (IsClientInPvP(iOwnerEntity)) - { - SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); - SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); - } - } - - return Plugin_Continue; -} - -public Hook_PvPProjectileSpawnPost(ent) -{ - decl String:sClass[64]; - GetEntityClassname(ent, sClass, sizeof(sClass)); - - new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); - new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); - - if (iOwnerEntity == -1 && iThrowerOffset != -1) - { - iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); - } - - if (IsValidClient(iOwnerEntity)) - { - if (IsClientInPvP(iOwnerEntity)) - { - SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); - SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); - } - } -} - -public PvP_OnPlayerSpawn(client) -{ - PvP_SetPlayerPvPState(client, false, false, false); - - if (IsPlayerAlive(client) && IsClientParticipating(client)) - { - if (!IsClientInGhostMode(client) && !g_bPlayerProxy[client]) - { - if (g_bPlayerEliminated[client] || g_bPlayerEscaped[client]) - { - new bool:bAutoSpawn = g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn]; - - if (bAutoSpawn) - { - g_hPlayerPvPRespawnTimer[client] = CreateTimer(0.12, Timer_TeleportPlayerToPvP, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - else - { - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; - } - } - } -} - -public PvP_OnPlayerDeath(client, bool:bFake) -{ - if (!bFake) - { - if (!IsClientInGhostMode(client) && !g_bPlayerProxy[client]) - { - new bool:bAutoSpawn = g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn]; - - if (bAutoSpawn) - { - if (g_bPlayerEliminated[client] || g_bPlayerEscaped[client]) - { - if (!IsRoundEnding()) - { - g_hPlayerPvPRespawnTimer[client] = CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - } - } - } -} - -public PvP_OnClientGhostModeEnable(client) -{ - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; -} - -public PvP_OnClientPutInPlay(client) -{ - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; -} - -public bool:Hook_ClientPvPShouldCollide(ent, collisiongroup, contentsmask, bool:originalResult) -{ - if (!g_bEnabled) return originalResult; - return true; -} - -public PvP_OnTriggerStartTouch(trigger, other) -{ - decl String:sName[64]; - GetEntPropString(trigger, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrContains(sName, "sf2_pvp_trigger", false) == 0) - { - if (IsValidClient(other) && IsPlayerAlive(other)) - { - g_bPlayerInPvPTrigger[other] = true; - - if (IsClientInPvP(other)) - { - // Player left and came back again, but is still in PvP mode. - g_iPlayerPvPTimerCount[other] = 0; - g_hPlayerPvPTimer[other] = INVALID_HANDLE; - } - else - { - PvP_SetPlayerPvPState(other, true); - } - } - } -} - -public PvP_OnTriggerEndTouch(trigger, other) -{ - decl String:sName[64]; - GetEntPropString(trigger, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrContains(sName, "sf2_pvp_trigger", false) == 0) - { - if (IsValidClient(other)) - { - g_bPlayerInPvPTrigger[other] = false; - - if (IsClientInPvP(other)) - { - g_iPlayerPvPTimerCount[other] = GetConVarInt(g_cvPvPArenaLeaveTime); - g_hPlayerPvPTimer[other] = CreateTimer(1.0, Timer_PlayerPvPLeaveCountdown, GetClientUserId(other), TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); - } - } - } -} - -/** - * Enables/Disables PvP mode on the player. - */ -PvP_SetPlayerPvPState(client, bool:bStatus, bool:bRemoveProjectiles=true, bool:bRegenerate=true) -{ - if (!IsValidClient(client)) return; - - new bool:bOldInPvP = g_bPlayerInPvP[client]; - if (bStatus == bOldInPvP) return; // no change - - g_bPlayerInPvP[client] = bStatus; - g_hPlayerPvPTimer[client] = INVALID_HANDLE; - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; - g_iPlayerPvPTimerCount[client] = 0; - - if (bRemoveProjectiles) - { - // Remove previous projectiles. - PvP_RemovePlayerProjectiles(client); - } - - if (bRegenerate) - { - // Regenerate player but keep health the same. - new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); - TF2_RegeneratePlayer(client); - SetEntProp(client, Prop_Data, "m_iHealth", iHealth); - SetEntProp(client, Prop_Send, "m_iHealth", iHealth); - } - - if (bStatus && GetConVarBool(g_cvPvPArenaPlayerCollisions)) - { - SDKHook(client, SDKHook_ShouldCollide, Hook_ClientPvPShouldCollide); - } - else - { - SDKUnhook(client, SDKHook_ShouldCollide, Hook_ClientPvPShouldCollide); - } -} - -static PvP_OnFlameEntityStartTouchPost(flame, other) -{ - if (IsValidClient(other)) - { - if ((IsRoundInWarmup() || IsClientInPvP(other)) && !IsRoundEnding()) - { - new iFlamethrower = GetEntPropEnt(flame, Prop_Data, "m_hOwnerEntity"); - if (IsValidEdict(iFlamethrower)) - { - new iOwnerEntity = GetEntPropEnt(iFlamethrower, Prop_Data, "m_hOwnerEntity"); - if (iOwnerEntity != other && IsValidClient(iOwnerEntity)) - { - if (IsRoundInWarmup() || IsClientInPvP(iOwnerEntity)) - { - if (GetClientTeam(other) == GetClientTeam(iOwnerEntity)) - { - TF2_IgnitePlayer(other, iOwnerEntity); - SDKHooks_TakeDamage(other, iOwnerEntity, iOwnerEntity, 7.0, IsClientCritBoosted(iOwnerEntity) ? (DMG_BURN | DMG_PREVENT_PHYSICS_FORCE | DMG_ACID) : DMG_BURN | DMG_PREVENT_PHYSICS_FORCE); - } - } - } - } - } - } -} - -/** - * Forcibly resets global vars of the player relating to PvP. Ignores checking. - */ -PvP_ForceResetPlayerPvPData(client) -{ - g_bPlayerInPvP[client] = false; - g_hPlayerPvPTimer[client] = INVALID_HANDLE; - g_iPlayerPvPTimerCount[client] = 0; - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; -} - -static PvP_RemovePlayerProjectiles(client) -{ - for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) - { - new ent = -1; - while ((ent = FindEntityByClassname(ent, g_sPvPProjectileClasses[i])) != -1) - { - new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); - new bool:bMine = false; - - new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); - if (iOwnerEntity == client) - { - bMine = true; - } - else if (iThrowerOffset != -1) - { - iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); - if (iOwnerEntity == client) - { - bMine = true; - } - } - - if (bMine) AcceptEntityInput(ent, "Kill"); - } - } -} - -public Action:Timer_TeleportPlayerToPvP(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerPvPRespawnTimer[client]) return; - g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; - - new Handle:hSpawnPointList = CreateArray(); - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - decl String:sName[32]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (!StrContains(sName, "sf2_pvp_spawnpoint", false)) - { - PushArrayCell(hSpawnPointList, ent); - } - } - - decl Float:flMins[3], Float:flMaxs[3]; - GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); - GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); - - new Handle:hClearSpawnPointList = CloneArray(hSpawnPointList); - for (new i = 0; i < GetArraySize(hSpawnPointList); i++) - { - new iEnt = GetArrayCell(hSpawnPointList, i); - - decl Float:flMyPos[3]; - GetEntPropVector(iEnt, Prop_Data, "m_vecAbsOrigin", flMyPos); - - if (IsSpaceOccupiedPlayer(flMyPos, flMins, flMaxs, client)) - { - new iIndex = FindValueInArray(hClearSpawnPointList, iEnt); - if (iIndex != -1) - { - RemoveFromArray(hClearSpawnPointList, iIndex); - } - } - } - - new iNum; - if ((iNum = GetArraySize(hClearSpawnPointList)) > 0) - { - ent = GetArrayCell(hClearSpawnPointList, GetRandomInt(0, iNum - 1)); - } - else if ((iNum = GetArraySize(hSpawnPointList)) > 0) - { - ent = GetArrayCell(hSpawnPointList, GetRandomInt(0, iNum - 1)); - } - - if (iNum > 0) - { - decl Float:flPos[3], Float:flAng[3]; - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); - GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); - TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); - - EmitAmbientSound(SF2_PVP_SPAWN_SOUND, flPos, _, SNDLEVEL_NORMAL, _, 1.0); - } - - CloseHandle(hSpawnPointList); - CloseHandle(hClearSpawnPointList); -} - -public Action:Timer_PlayerPvPLeaveCountdown(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerPvPTimer[client]) return Plugin_Stop; - - if (!IsClientInPvP(client)) return Plugin_Stop; - - if (g_iPlayerPvPTimerCount[client] <= 0) - { - PvP_SetPlayerPvPState(client, false); - return Plugin_Stop; - } - - g_iPlayerPvPTimerCount[client]--; - - //if (!g_bPlayerProxyAvailableInForce[client]) - { - SetHudTextParams(-1.0, 0.75, - 1.0, - 255, 255, 255, 255, - _, - _, - 0.25, 1.25); - - ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Exiting PvP Arena", client, g_iPlayerPvPTimerCount[client]); - } - - return Plugin_Continue; -} - -bool:IsClientInPvP(client) -{ - return g_bPlayerInPvP[client]; -} - - -// API - -public PvP_InitializeAPI() -{ - CreateNative("SF2_IsClientInPvP", Native_IsClientInPvP); -} - -public Native_IsClientInPvP(Handle:plugin, numParams) -{ - return IsClientInPvP(GetNativeCell(1)); +#if defined _sf2_pvp_included + #endinput +#endif +#define _sf2_pvp_included + + +#define SF2_PVP_SPAWN_SOUND "items/spawn_item.wav" + +new Handle:g_cvPvPArenaLeaveTime; +new Handle:g_cvPvPArenaPlayerCollisions; + +static const String:g_sPvPProjectileClasses[][] = +{ + "tf_projectile_rocket", + "tf_projectile_sentryrocket", + "tf_projectile_arrow", + "tf_projectile_stun_ball", + "tf_projectile_ball_ornament", + "tf_projectile_cleaver", + "tf_projectile_energy_ball", + "tf_projectile_energy_ring", + "tf_projectile_flare", + "tf_projectile_healing_bolt", + "tf_projectile_jar", + "tf_projectile_jar_milk", + "tf_projectile_pipe", + "tf_projectile_pipe_remote", + "tf_projectile_syringe" +}; + +static bool:g_bPlayerInPvP[MAXPLAYERS + 1]; +static Handle:g_hPlayerPvPTimer[MAXPLAYERS + 1]; +static Handle:g_hPlayerPvPRespawnTimer[MAXPLAYERS + 1]; +static g_iPlayerPvPTimerCount[MAXPLAYERS + 1]; +static bool:g_bPlayerInPvPTrigger[MAXPLAYERS + 1]; + +static Handle:g_hPvPFlameEntities; + +enum +{ + PvPFlameEntData_EntRef = 0, + PvPFlameEntData_LastHitEntRef, + PvPFlameEntData_MaxStats +}; + +public PvP_Initialize() +{ + g_cvPvPArenaLeaveTime = CreateConVar("sf2_player_pvparena_leavetime", "3"); + g_cvPvPArenaPlayerCollisions = CreateConVar("sf2_player_pvparena_collisions", "1"); + + g_hPvPFlameEntities = CreateArray(PvPFlameEntData_MaxStats); +} + +public PvP_SetupMenus() +{ + g_hMenuSettingsPvP = CreateMenu(Menu_SettingsPvP); + SetMenuTitle(g_hMenuSettingsPvP, "%t%t\n \n", "SF2 Prefix", "SF2 Settings PvP Menu Title"); + AddMenuItem(g_hMenuSettingsPvP, "0", "Toggle automatic spawning"); + SetMenuExitBackButton(g_hMenuSettingsPvP, true); +} + +public PvP_OnMapStart() +{ + ClearArray(g_hPvPFlameEntities); +} + +public PvP_Precache() +{ + PrecacheSound2(SF2_PVP_SPAWN_SOUND); +} + +public PvP_OnClientPutInServer(client) +{ + PvP_ForceResetPlayerPvPData(client); +} + +public PvP_OnClientDisconnect(client) +{ + PvP_SetPlayerPvPState(client, false, false, false); +} + +public PvP_OnGameFrame() +{ + // Process through PvP projectiles. + for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) + { + new ent = -1; + while ((ent = FindEntityByClassname(ent, g_sPvPProjectileClasses[i])) != -1) + { + new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); + new bool:bChangeProjectileTeam = false; + + new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); + if (IsValidClient(iOwnerEntity) && IsClientInPvP(iOwnerEntity)) + { + bChangeProjectileTeam = true; + } + else if (iThrowerOffset != -1) + { + iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); + if (IsValidClient(iOwnerEntity) && IsClientInPvP(iOwnerEntity)) + { + bChangeProjectileTeam = true; + } + } + + if (bChangeProjectileTeam) + { + SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); + SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); + } + } + } + + // Process through PvP flame entities. + { + static Float:flMins[3] = { -6.0, ... }; + static Float:flMaxs[3] = { 6.0, ... }; + + decl Float:flOrigin[3]; + + new Handle:hTrace = INVALID_HANDLE; + new ent = -1; + new iOwnerEntity = INVALID_ENT_REFERENCE; + new iHitEntity = INVALID_ENT_REFERENCE; + + while ((ent = FindEntityByClassname(ent, "tf_flame")) != -1) + { + iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); + + if (IsValidEdict(iOwnerEntity)) + { + // tf_flame's initial owner SHOULD be the flamethrower that it originates from. + // If not, then something's completely bogus. + + iOwnerEntity = GetEntPropEnt(iOwnerEntity, Prop_Data, "m_hOwnerEntity"); + } + + if (IsValidClient(iOwnerEntity) && (IsRoundInWarmup() || IsClientInPvP(iOwnerEntity))) + { + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flOrigin); + + hTrace = TR_TraceHullFilterEx(flOrigin, flOrigin, flMins, flMaxs, MASK_PLAYERSOLID, TraceRayDontHitEntity, iOwnerEntity); + iHitEntity = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (IsValidEntity(iHitEntity)) + { + new entref = EntIndexToEntRef(ent); + + new iIndex = FindValueInArray(g_hPvPFlameEntities, entref); + if (iIndex != -1) + { + new iLastHitEnt = EntRefToEntIndex(GetArrayCell(g_hPvPFlameEntities, iIndex, PvPFlameEntData_LastHitEntRef)); + + if (iHitEntity != iLastHitEnt) + { + SetArrayCell(g_hPvPFlameEntities, iIndex, EntIndexToEntRef(iHitEntity), PvPFlameEntData_LastHitEntRef); + PvP_OnFlameEntityStartTouchPost(ent, iHitEntity); + } + } + } + } + } + } +} + +public PvP_OnEntityCreated(ent, const String:sClassname[]) +{ + if (StrEqual(sClassname, "tf_flame", false)) + { + new iIndex = PushArrayCell(g_hPvPFlameEntities, EntIndexToEntRef(ent)); + if (iIndex != -1) + { + SetArrayCell(g_hPvPFlameEntities, iIndex, INVALID_ENT_REFERENCE, PvPFlameEntData_LastHitEntRef); + } + } + else + { + for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) + { + if (StrEqual(sClassname, g_sPvPProjectileClasses[i], false)) + { + SDKHook(ent, SDKHook_Spawn, Hook_PvPProjectileSpawn); + SDKHook(ent, SDKHook_SpawnPost, Hook_PvPProjectileSpawnPost); + break; + } + } + } +} + +public PvP_OnEntityDestroyed(ent, const String:sClassname[]) +{ + if (StrEqual(sClassname, "tf_flame", false)) + { + new entref = EntIndexToEntRef(ent); + new iIndex = FindValueInArray(g_hPvPFlameEntities, entref); + if (iIndex != -1) + { + RemoveFromArray(g_hPvPFlameEntities, iIndex); + } + } +} + +public Action:Hook_PvPProjectileSpawn(ent) +{ + decl String:sClass[64]; + GetEntityClassname(ent, sClass, sizeof(sClass)); + + new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); + new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); + + if (iOwnerEntity == -1 && iThrowerOffset != -1) + { + iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); + } + + if (IsValidClient(iOwnerEntity)) + { + if (IsClientInPvP(iOwnerEntity)) + { + SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); + SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); + } + } + + return Plugin_Continue; +} + +public Hook_PvPProjectileSpawnPost(ent) +{ + decl String:sClass[64]; + GetEntityClassname(ent, sClass, sizeof(sClass)); + + new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); + new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); + + if (iOwnerEntity == -1 && iThrowerOffset != -1) + { + iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); + } + + if (IsValidClient(iOwnerEntity)) + { + if (IsClientInPvP(iOwnerEntity)) + { + SetEntProp(ent, Prop_Data, "m_iInitialTeamNum", 0); + SetEntProp(ent, Prop_Send, "m_iTeamNum", 0); + } + } +} + +public PvP_OnPlayerSpawn(client) +{ + PvP_SetPlayerPvPState(client, false, false, false); + + if (IsPlayerAlive(client) && IsClientParticipating(client)) + { + if (!IsClientInGhostMode(client) && !g_bPlayerProxy[client]) + { + if (g_bPlayerEliminated[client] || g_bPlayerEscaped[client]) + { + new bool:bAutoSpawn = g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn]; + + if (bAutoSpawn) + { + g_hPlayerPvPRespawnTimer[client] = CreateTimer(0.12, Timer_TeleportPlayerToPvP, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + else + { + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; + } + } + } +} + +public PvP_OnPlayerDeath(client, bool:bFake) +{ + if (!bFake) + { + if (!IsClientInGhostMode(client) && !g_bPlayerProxy[client]) + { + new bool:bAutoSpawn = g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn]; + + if (bAutoSpawn) + { + if (g_bPlayerEliminated[client] || g_bPlayerEscaped[client]) + { + if (!IsRoundEnding()) + { + g_hPlayerPvPRespawnTimer[client] = CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + } + } + } +} + +public PvP_OnClientGhostModeEnable(client) +{ + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; +} + +public PvP_OnClientPutInPlay(client) +{ + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; +} + +public bool:Hook_ClientPvPShouldCollide(ent, collisiongroup, contentsmask, bool:originalResult) +{ + if (!g_bEnabled) return originalResult; + return true; +} + +public PvP_OnTriggerStartTouch(trigger, other) +{ + decl String:sName[64]; + GetEntPropString(trigger, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrContains(sName, "sf2_pvp_trigger", false) == 0) + { + if (IsValidClient(other) && IsPlayerAlive(other)) + { + g_bPlayerInPvPTrigger[other] = true; + + if (IsClientInPvP(other)) + { + // Player left and came back again, but is still in PvP mode. + g_iPlayerPvPTimerCount[other] = 0; + g_hPlayerPvPTimer[other] = INVALID_HANDLE; + } + else + { + PvP_SetPlayerPvPState(other, true); + } + } + } +} + +public PvP_OnTriggerEndTouch(trigger, other) +{ + decl String:sName[64]; + GetEntPropString(trigger, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrContains(sName, "sf2_pvp_trigger", false) == 0) + { + if (IsValidClient(other)) + { + g_bPlayerInPvPTrigger[other] = false; + + if (IsClientInPvP(other)) + { + g_iPlayerPvPTimerCount[other] = GetConVarInt(g_cvPvPArenaLeaveTime); + g_hPlayerPvPTimer[other] = CreateTimer(1.0, Timer_PlayerPvPLeaveCountdown, GetClientUserId(other), TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); + } + } + } +} + +/** + * Enables/Disables PvP mode on the player. + */ +PvP_SetPlayerPvPState(client, bool:bStatus, bool:bRemoveProjectiles=true, bool:bRegenerate=true) +{ + if (!IsValidClient(client)) return; + + new bool:bOldInPvP = g_bPlayerInPvP[client]; + if (bStatus == bOldInPvP) return; // no change + + g_bPlayerInPvP[client] = bStatus; + g_hPlayerPvPTimer[client] = INVALID_HANDLE; + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; + g_iPlayerPvPTimerCount[client] = 0; + + if (bRemoveProjectiles) + { + // Remove previous projectiles. + PvP_RemovePlayerProjectiles(client); + } + + if (bRegenerate) + { + // Regenerate player but keep health the same. + new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); + TF2_RegeneratePlayer(client); + SetEntProp(client, Prop_Data, "m_iHealth", iHealth); + SetEntProp(client, Prop_Send, "m_iHealth", iHealth); + } + + if (bStatus && GetConVarBool(g_cvPvPArenaPlayerCollisions)) + { + SDKHook(client, SDKHook_ShouldCollide, Hook_ClientPvPShouldCollide); + } + else + { + SDKUnhook(client, SDKHook_ShouldCollide, Hook_ClientPvPShouldCollide); + } +} + +static PvP_OnFlameEntityStartTouchPost(flame, other) +{ + if (IsValidClient(other)) + { + if ((IsRoundInWarmup() || IsClientInPvP(other)) && !IsRoundEnding()) + { + new iFlamethrower = GetEntPropEnt(flame, Prop_Data, "m_hOwnerEntity"); + if (IsValidEdict(iFlamethrower)) + { + new iOwnerEntity = GetEntPropEnt(iFlamethrower, Prop_Data, "m_hOwnerEntity"); + if (iOwnerEntity != other && IsValidClient(iOwnerEntity)) + { + if (IsRoundInWarmup() || IsClientInPvP(iOwnerEntity)) + { + if (GetClientTeam(other) == GetClientTeam(iOwnerEntity)) + { + TF2_IgnitePlayer(other, iOwnerEntity); + SDKHooks_TakeDamage(other, iOwnerEntity, iOwnerEntity, 7.0, IsClientCritBoosted(iOwnerEntity) ? (DMG_BURN | DMG_PREVENT_PHYSICS_FORCE | DMG_ACID) : DMG_BURN | DMG_PREVENT_PHYSICS_FORCE); + } + } + } + } + } + } +} + +/** + * Forcibly resets global vars of the player relating to PvP. Ignores checking. + */ +PvP_ForceResetPlayerPvPData(client) +{ + g_bPlayerInPvP[client] = false; + g_hPlayerPvPTimer[client] = INVALID_HANDLE; + g_iPlayerPvPTimerCount[client] = 0; + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; +} + +static PvP_RemovePlayerProjectiles(client) +{ + for (new i = 0; i < sizeof(g_sPvPProjectileClasses); i++) + { + new ent = -1; + while ((ent = FindEntityByClassname(ent, g_sPvPProjectileClasses[i])) != -1) + { + new iThrowerOffset = FindDataMapOffs(ent, "m_hThrower"); + new bool:bMine = false; + + new iOwnerEntity = GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity"); + if (iOwnerEntity == client) + { + bMine = true; + } + else if (iThrowerOffset != -1) + { + iOwnerEntity = GetEntDataEnt2(ent, iThrowerOffset); + if (iOwnerEntity == client) + { + bMine = true; + } + } + + if (bMine) AcceptEntityInput(ent, "Kill"); + } + } +} + +public Action:Timer_TeleportPlayerToPvP(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerPvPRespawnTimer[client]) return; + g_hPlayerPvPRespawnTimer[client] = INVALID_HANDLE; + + new Handle:hSpawnPointList = CreateArray(); + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + decl String:sName[32]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (!StrContains(sName, "sf2_pvp_spawnpoint", false)) + { + PushArrayCell(hSpawnPointList, ent); + } + } + + decl Float:flMins[3], Float:flMaxs[3]; + GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); + GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); + + new Handle:hClearSpawnPointList = CloneArray(hSpawnPointList); + for (new i = 0; i < GetArraySize(hSpawnPointList); i++) + { + new iEnt = GetArrayCell(hSpawnPointList, i); + + decl Float:flMyPos[3]; + GetEntPropVector(iEnt, Prop_Data, "m_vecAbsOrigin", flMyPos); + + if (IsSpaceOccupiedPlayer(flMyPos, flMins, flMaxs, client)) + { + new iIndex = FindValueInArray(hClearSpawnPointList, iEnt); + if (iIndex != -1) + { + RemoveFromArray(hClearSpawnPointList, iIndex); + } + } + } + + new iNum; + if ((iNum = GetArraySize(hClearSpawnPointList)) > 0) + { + ent = GetArrayCell(hClearSpawnPointList, GetRandomInt(0, iNum - 1)); + } + else if ((iNum = GetArraySize(hSpawnPointList)) > 0) + { + ent = GetArrayCell(hSpawnPointList, GetRandomInt(0, iNum - 1)); + } + + if (iNum > 0) + { + decl Float:flPos[3], Float:flAng[3]; + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); + GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); + TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); + + EmitAmbientSound(SF2_PVP_SPAWN_SOUND, flPos, _, SNDLEVEL_NORMAL, _, 1.0); + } + + CloseHandle(hSpawnPointList); + CloseHandle(hClearSpawnPointList); +} + +public Action:Timer_PlayerPvPLeaveCountdown(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerPvPTimer[client]) return Plugin_Stop; + + if (!IsClientInPvP(client)) return Plugin_Stop; + + if (g_iPlayerPvPTimerCount[client] <= 0) + { + PvP_SetPlayerPvPState(client, false); + return Plugin_Stop; + } + + g_iPlayerPvPTimerCount[client]--; + + //if (!g_bPlayerProxyAvailableInForce[client]) + { + SetHudTextParams(-1.0, 0.75, + 1.0, + 255, 255, 255, 255, + _, + _, + 0.25, 1.25); + + ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Exiting PvP Arena", client, g_iPlayerPvPTimerCount[client]); + } + + return Plugin_Continue; +} + +bool:IsClientInPvP(client) +{ + return g_bPlayerInPvP[client]; +} + + +// API + +public PvP_InitializeAPI() +{ + CreateNative("SF2_IsClientInPvP", Native_IsClientInPvP); +} + +public Native_IsClientInPvP(Handle:plugin, numParams) +{ + return IsClientInPvP(GetNativeCell(1)); } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/pvp/menus.sp b/addons/sourcemod/scripting/rytp_horror/pvp/menus.sp index 7fe9882..d00ac8f 100644 --- a/addons/sourcemod/scripting/rytp_horror/pvp/menus.sp +++ b/addons/sourcemod/scripting/rytp_horror/pvp/menus.sp @@ -1,62 +1,62 @@ -#if defined _sf2_pvp_menus - #endinput -#endif - -#define _sf2_pvp_menus - -new Handle:g_hMenuSettingsPvP; - -public Menu_SettingsPvP(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 0: - { - decl String:sBuffer[512]; - Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings PvP Spawn Menu Title", param1); - - new Handle:hPanel = CreatePanel(); - SetPanelTitle(hPanel, sBuffer); - - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); - DrawPanelItem(hPanel, sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); - DrawPanelItem(hPanel, sBuffer); - - SendPanelToClient(hPanel, param1, Panel_SettingsPvPSpawn, 30); - CloseHandle(hPanel); - } - } - } - else if (action == MenuAction_Cancel) - { - if (param2 == MenuCancel_ExitBack) - { - DisplayMenu(g_hMenuSettings, param1, 30); - } - } -} - -public Panel_SettingsPvPSpawn(Handle:menu, MenuAction:action, param1, param2) -{ - if (action == MenuAction_Select) - { - switch (param2) - { - case 1: - { - g_iPlayerPreferences[param1][PlayerPreference_PvPAutoSpawn] = true; - CPrintToChat(param1, "%T", "SF2 PvP Spawn Accept", param1); - } - case 2: - { - g_iPlayerPreferences[param1][PlayerPreference_PvPAutoSpawn] = false; - CPrintToChat(param1, "%T", "SF2 PvP Spawn Decline", param1); - } - } - - DisplayMenu(g_hMenuSettings, param1, 30); - } +#if defined _sf2_pvp_menus + #endinput +#endif + +#define _sf2_pvp_menus + +new Handle:g_hMenuSettingsPvP; + +public Menu_SettingsPvP(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 0: + { + decl String:sBuffer[512]; + Format(sBuffer, sizeof(sBuffer), "%T\n \n", "SF2 Settings PvP Spawn Menu Title", param1); + + new Handle:hPanel = CreatePanel(); + SetPanelTitle(hPanel, sBuffer); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", param1); + DrawPanelItem(hPanel, sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", param1); + DrawPanelItem(hPanel, sBuffer); + + SendPanelToClient(hPanel, param1, Panel_SettingsPvPSpawn, 30); + CloseHandle(hPanel); + } + } + } + else if (action == MenuAction_Cancel) + { + if (param2 == MenuCancel_ExitBack) + { + DisplayMenu(g_hMenuSettings, param1, 30); + } + } +} + +public Panel_SettingsPvPSpawn(Handle:menu, MenuAction:action, param1, param2) +{ + if (action == MenuAction_Select) + { + switch (param2) + { + case 1: + { + g_iPlayerPreferences[param1][PlayerPreference_PvPAutoSpawn] = true; + CPrintToChat(param1, "%T", "SF2 PvP Spawn Accept", param1); + } + case 2: + { + g_iPlayerPreferences[param1][PlayerPreference_PvPAutoSpawn] = false; + CPrintToChat(param1, "%T", "SF2 PvP Spawn Decline", param1); + } + } + + DisplayMenu(g_hMenuSettings, param1, 30); + } } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/specialround.sp b/addons/sourcemod/scripting/rytp_horror/specialround.sp index a6e2d5d..f930500 100644 --- a/addons/sourcemod/scripting/rytp_horror/specialround.sp +++ b/addons/sourcemod/scripting/rytp_horror/specialround.sp @@ -1,379 +1,378 @@ -#if defined _sf2_specialround_included - #endinput -#endif -#define _sf2_specialround_included - -#define SR_CYCLELENGTH 10.0 -#define SR_STARTDELAY 1.25 -#define SR_MUSIC "rytp_horror/specialround_music.mp3" -#define SR_SOUND_SELECT "rytp_horror/specialround_select.mp3" - -#define FILE_SPECIALROUNDS "configs/sf2/specialrounds.cfg" - -static Handle:g_hSpecialRoundCycleNames = INVALID_HANDLE; - -static Handle:g_hSpecialRoundTimer = INVALID_HANDLE; -static g_iSpecialRoundCycleNum = 0; -static Float:g_flSpecialRoundCycleEndTime = -1.0; - -ReloadSpecialRounds() -{ - if (g_hSpecialRoundCycleNames == INVALID_HANDLE) - { - g_hSpecialRoundCycleNames = CreateArray(128); - } - - ClearArray(g_hSpecialRoundCycleNames); - - if (g_hSpecialRoundsConfig != INVALID_HANDLE) - { - CloseHandle(g_hSpecialRoundsConfig); - g_hSpecialRoundsConfig = INVALID_HANDLE; - } - - decl String:buffer[PLATFORM_MAX_PATH]; - BuildPath(Path_SM, buffer, sizeof(buffer), FILE_SPECIALROUNDS); - new Handle:kv = CreateKeyValues("root"); - if (!FileToKeyValues(kv, buffer)) - { - CloseHandle(kv); - LogError("Failed to load special rounds! File %s not found!", FILE_SPECIALROUNDS); - } - else - { - g_hSpecialRoundsConfig = kv; - LogMessage("Loaded special rounds file!"); - - // Load names for the cycle. - decl String:sBuffer[128]; - SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLETROUBLE, sBuffer, sizeof(sBuffer)); - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - - SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLETROUBLE, sBuffer, sizeof(sBuffer)); - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - /* - SpecialRoundGetDescriptionHud(SPECIALROUND_SINGLEPLAYER, sBuffer, sizeof(sBuffer)); - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - - SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLEMAXPLAYERS, sBuffer, sizeof(sBuffer)); - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - */ - SpecialRoundGetDescriptionHud(SPECIALROUND_LIGHTSOUT, sBuffer, sizeof(sBuffer)); - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - - KvRewind(kv); - if (KvJumpToKey(kv, "jokes")) - { - if (KvGotoFirstSubKey(kv, false)) - { - do - { - KvGetString(kv, NULL_STRING, sBuffer, sizeof(sBuffer)); - if (strlen(sBuffer) > 0) - { - PushArrayString(g_hSpecialRoundCycleNames, sBuffer); - } - } - while (KvGotoNextKey(kv, false)); - } - } - - SortADTArray(g_hSpecialRoundCycleNames, Sort_Random, Sort_String); - } -} - -stock SpecialRoundGetDescriptionHud(iSpecialRound, String:buffer[], bufferlen) -{ - strcopy(buffer, bufferlen, ""); - - if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; - - KvRewind(g_hSpecialRoundsConfig); - decl String:sSpecialRound[32]; - IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); - - if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; - - KvGetString(g_hSpecialRoundsConfig, "display_text_hud", buffer, bufferlen); -} - -stock SpecialRoundGetDescriptionChat(iSpecialRound, String:buffer[], bufferlen) -{ - strcopy(buffer, bufferlen, ""); - - if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; - - KvRewind(g_hSpecialRoundsConfig); - decl String:sSpecialRound[32]; - IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); - - if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; - - KvGetString(g_hSpecialRoundsConfig, "display_text_chat", buffer, bufferlen); -} - -stock SpecialRoundGetIconHud(iSpecialRound, String:buffer[], bufferlen) -{ - strcopy(buffer, bufferlen, ""); - - if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; - - KvRewind(g_hSpecialRoundsConfig); - decl String:sSpecialRound[32]; - IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); - - if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; - - KvGetString(g_hSpecialRoundsConfig, "display_icon_hud", buffer, bufferlen); -} - -stock bool:SpecialRoundCanBeSelected(iSpecialRound) -{ - if (g_hSpecialRoundsConfig == INVALID_HANDLE) return false; - - KvRewind(g_hSpecialRoundsConfig); - decl String:sSpecialRound[32]; - IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); - - if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return false; - - return bool:KvGetNum(g_hSpecialRoundsConfig, "enabled", 1); -} - -public Action:Timer_SpecialRoundCycle(Handle:timer) -{ - if (timer != g_hSpecialRoundTimer) return Plugin_Stop; - - if (GetGameTime() >= g_flSpecialRoundCycleEndTime) - { - SpecialRoundCycleFinish(); - return Plugin_Stop; - } - - decl String:sBuffer[128]; - GetArrayString(g_hSpecialRoundCycleNames, g_iSpecialRoundCycleNum, sBuffer, sizeof(sBuffer)); - - GameTextTFMessage(sBuffer); - - g_iSpecialRoundCycleNum++; - if (g_iSpecialRoundCycleNum >= GetArraySize(g_hSpecialRoundCycleNames)) - { - g_iSpecialRoundCycleNum = 0; - } - - return Plugin_Continue; -} - -public Action:Timer_SpecialRoundStart(Handle:timer) -{ - if (timer != g_hSpecialRoundTimer) return; - if (!g_bSpecialRound) return; - - SpecialRoundStart(); -} - -/* -public Action:Timer_SpecialRoundAttribute(Handle:timer) -{ - if (timer != g_hSpecialRoundTimer) return Plugin_Stop; - if (!g_bSpecialRound) return Plugin_Stop; - - new iCond = -1; - - switch (g_iSpecialRoundType) - { - case SPECIALROUND_DEFENSEBUFF: iCond = _:TFCond_DefenseBuffed; - case SPECIALROUND_MARKEDFORDEATH: iCond = _:TFCond_MarkedForDeath; - } - - if (iCond != -1) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || g_bPlayerGhostMode[i]) continue; - - TF2_AddCondition(i, TFCond:iCond, 0.8); - } - } - - return Plugin_Continue; -} -*/ - -SpecialRoundCycleStart() -{ - if (!g_bSpecialRound) return; - - EmitSoundToAll(SR_MUSIC, _, MUSIC_CHAN); - g_iSpecialRoundType = 0; - g_iSpecialRoundCycleNum = 0; - g_flSpecialRoundCycleEndTime = GetGameTime() + SR_CYCLELENGTH; - g_hSpecialRoundTimer = CreateTimer(0.12, Timer_SpecialRoundCycle, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -SpecialRoundCycleFinish() -{ - //EmitSoundToAll(SR_SOUND_SELECT, _, SNDCHAN_AUTO); - - new iOverride = GetConVarInt(g_cvSpecialRoundOverride); - if (iOverride >= 1 && iOverride < SPECIALROUND_MAXROUNDS) - { - g_iSpecialRoundType = iOverride; - } - else - { - new Handle:hEnabledRounds = CreateArray(); - - if (GetArraySize(GetSelectableBossProfileList()) > 0) - { - PushArrayCell(hEnabledRounds, SPECIALROUND_DOUBLETROUBLE); - } - /* - if (GetActivePlayerCount() <= GetConVarInt(g_cvMaxPlayers) * 2) - { - PushArrayCell(hEnabledRounds, SPECIALROUND_DOUBLEMAXPLAYERS); - } - - if (GetActivePlayerCount() > 1) - { - PushArrayCell(hEnabledRounds, SPECIALROUND_SINGLEPLAYER); - } - */ - - PushArrayCell(hEnabledRounds, SPECIALROUND_INSANEDIFFICULTY); - PushArrayCell(hEnabledRounds, SPECIALROUND_LIGHTSOUT); - - g_iSpecialRoundType = GetArrayCell(hEnabledRounds, GetRandomInt(0, GetArraySize(hEnabledRounds) - 1)); - - CloseHandle(hEnabledRounds); - } - - SetConVarInt(g_cvSpecialRoundOverride, -1); - - decl String:sDescHud[64]; - SpecialRoundGetDescriptionHud(g_iSpecialRoundType, sDescHud, sizeof(sDescHud)); - - decl String:sIconHud[64]; - SpecialRoundGetIconHud(g_iSpecialRoundType, sIconHud, sizeof(sIconHud)); - - decl String:sDescChat[64]; - SpecialRoundGetDescriptionChat(g_iSpecialRoundType, sDescChat, sizeof(sDescChat)); - - GameTextTFMessage(sDescHud, sIconHud); - CPrintToChatAll("%t", "SF2 Special Round Announce Chat", sDescChat); // For those who are using minimized HUD... - - g_hSpecialRoundTimer = CreateTimer(SR_STARTDELAY, Timer_SpecialRoundStart, _, TIMER_FLAG_NO_MAPCHANGE); -} - -SpecialRoundStart() -{ - if (!g_bSpecialRound) return; - if (g_iSpecialRoundType < 1 || g_iSpecialRoundType >= SPECIALROUND_MAXROUNDS) return; - - // What to do with the timer... - switch (g_iSpecialRoundType) - { - /* - case SPECIALROUND_DEFENSEBUFF, SPECIALROUND_MARKEDFORDEATH: - { - g_hSpecialRoundTimer = CreateTimer(0.5, Timer_SpecialRoundAttribute, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - */ - default: - { - g_hSpecialRoundTimer = INVALID_HANDLE; - } - } - - switch (g_iSpecialRoundType) - { - case SPECIALROUND_DOUBLETROUBLE: - { - decl String:sBuffer[SF2_MAX_PROFILE_NAME_LENGTH]; - new Handle:hSelectableBosses = GetSelectableBossProfileList(); - - if (GetArraySize(hSelectableBosses) > 0) - { - GetArrayString(hSelectableBosses, GetRandomInt(0, GetArraySize(hSelectableBosses) - 1), sBuffer, sizeof(sBuffer)); - AddProfile(sBuffer); - } - } - case SPECIALROUND_INSANEDIFFICULTY: - { - SetConVarString(g_cvDifficulty, "3"); // Override difficulty to Insane. - } - /* - case SPECIALROUND_SINGLEPLAYER: - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - ClientUpdateListeningFlags(i); - } - } - case SPECIALROUND_DOUBLEMAXPLAYERS: - { - ForceInNextPlayersInQueue(GetConVarInt(g_cvMaxPlayers)); - SetConVarString(g_cvDifficulty, "3"); // Override difficulty to Insane. - } - */ - case SPECIALROUND_LIGHTSOUT: - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - ClientResetFlashlight(i); - ClientActivateUltravision(i); - } - } - } - } -} - -public Action:Timer_DisplaySpecialRound(Handle:timer) -{ - decl String:sDescHud[64]; - SpecialRoundGetDescriptionHud(g_iSpecialRoundType, sDescHud, sizeof(sDescHud)); - - decl String:sIconHud[64]; - SpecialRoundGetIconHud(g_iSpecialRoundType, sIconHud, sizeof(sIconHud)); - - decl String:sDescChat[64]; - SpecialRoundGetDescriptionChat(g_iSpecialRoundType, sDescChat, sizeof(sDescChat)); - - GameTextTFMessage(sDescHud, sIconHud); - CPrintToChatAll("%t", "SF2 Special Round Announce Chat", sDescChat); // For those who are using minimized HUD... -} - -SpecialRoundReset() -{ - g_iSpecialRoundType = 0; - g_hSpecialRoundTimer = INVALID_HANDLE; - g_iSpecialRoundCycleNum = 0; - g_flSpecialRoundCycleEndTime = -1.0; -} - -bool:IsSpecialRoundRunning() -{ - return g_bSpecialRound; -} - -public SpecialRoundInitializeAPI() -{ - CreateNative("SF2_IsSpecialRoundRunning", Native_IsSpecialRoundRunning); - CreateNative("SF2_GetSpecialRoundType", Native_GetSpecialRoundType); -} - -public Native_IsSpecialRoundRunning(Handle:plugin, numParams) -{ - return g_bSpecialRound; -} - -public Native_GetSpecialRoundType(Handle:plugin, numParams) -{ - return g_iSpecialRoundType; +#if defined _sf2_specialround_included + #endinput +#endif +#define _sf2_specialround_included + +#define SR_CYCLELENGTH 10.0 +#define SR_STARTDELAY 1.25 +#define SR_MUSIC "rytp_horror/specialround_music.mp3" +#define SR_SOUND_SELECT "rytp_horror/specialround_select.mp3" + +#define FILE_SPECIALROUNDS "configs/sf2/specialrounds.cfg" + +static Handle:g_hSpecialRoundCycleNames = INVALID_HANDLE; + +static Handle:g_hSpecialRoundTimer = INVALID_HANDLE; +static g_iSpecialRoundCycleNum = 0; +static Float:g_flSpecialRoundCycleEndTime = -1.0; + +ReloadSpecialRounds() +{ + if (g_hSpecialRoundCycleNames == INVALID_HANDLE) + { + g_hSpecialRoundCycleNames = CreateArray(128); + } + + ClearArray(g_hSpecialRoundCycleNames); + + if (g_hSpecialRoundsConfig != INVALID_HANDLE) + { + CloseHandle(g_hSpecialRoundsConfig); + g_hSpecialRoundsConfig = INVALID_HANDLE; + } + + decl String:buffer[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, buffer, sizeof(buffer), FILE_SPECIALROUNDS); + new Handle:kv = CreateKeyValues("root"); + if (!FileToKeyValues(kv, buffer)) + { + CloseHandle(kv); + LogError("Failed to load special rounds! File %s not found!", FILE_SPECIALROUNDS); + } + else + { + g_hSpecialRoundsConfig = kv; + LogMessage("Loaded special rounds file!"); + + // Load names for the cycle. + decl String:sBuffer[128]; + SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLETROUBLE, sBuffer, sizeof(sBuffer)); + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + + SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLETROUBLE, sBuffer, sizeof(sBuffer)); + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + + SpecialRoundGetDescriptionHud(SPECIALROUND_SINGLEPLAYER, sBuffer, sizeof(sBuffer)); + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + + SpecialRoundGetDescriptionHud(SPECIALROUND_DOUBLEMAXPLAYERS, sBuffer, sizeof(sBuffer)); + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + + SpecialRoundGetDescriptionHud(SPECIALROUND_LIGHTSOUT, sBuffer, sizeof(sBuffer)); + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + + KvRewind(kv); + if (KvJumpToKey(kv, "jokes")) + { + if (KvGotoFirstSubKey(kv, false)) + { + do + { + KvGetString(kv, NULL_STRING, sBuffer, sizeof(sBuffer)); + if (strlen(sBuffer) > 0) + { + PushArrayString(g_hSpecialRoundCycleNames, sBuffer); + } + } + while (KvGotoNextKey(kv, false)); + } + } + + SortADTArray(g_hSpecialRoundCycleNames, Sort_Random, Sort_String); + } +} + +stock SpecialRoundGetDescriptionHud(iSpecialRound, String:buffer[], bufferlen) +{ + strcopy(buffer, bufferlen, ""); + + if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; + + KvRewind(g_hSpecialRoundsConfig); + decl String:sSpecialRound[32]; + IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); + + if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; + + KvGetString(g_hSpecialRoundsConfig, "display_text_hud", buffer, bufferlen); +} + +stock SpecialRoundGetDescriptionChat(iSpecialRound, String:buffer[], bufferlen) +{ + strcopy(buffer, bufferlen, ""); + + if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; + + KvRewind(g_hSpecialRoundsConfig); + decl String:sSpecialRound[32]; + IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); + + if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; + + KvGetString(g_hSpecialRoundsConfig, "display_text_chat", buffer, bufferlen); +} + +stock SpecialRoundGetIconHud(iSpecialRound, String:buffer[], bufferlen) +{ + strcopy(buffer, bufferlen, ""); + + if (g_hSpecialRoundsConfig == INVALID_HANDLE) return; + + KvRewind(g_hSpecialRoundsConfig); + decl String:sSpecialRound[32]; + IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); + + if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return; + + KvGetString(g_hSpecialRoundsConfig, "display_icon_hud", buffer, bufferlen); +} + +stock bool:SpecialRoundCanBeSelected(iSpecialRound) +{ + if (g_hSpecialRoundsConfig == INVALID_HANDLE) return false; + + KvRewind(g_hSpecialRoundsConfig); + decl String:sSpecialRound[32]; + IntToString(iSpecialRound, sSpecialRound, sizeof(sSpecialRound)); + + if (!KvJumpToKey(g_hSpecialRoundsConfig, sSpecialRound)) return false; + + return bool:KvGetNum(g_hSpecialRoundsConfig, "enabled", 1); +} + +public Action:Timer_SpecialRoundCycle(Handle:timer) +{ + if (timer != g_hSpecialRoundTimer) return Plugin_Stop; + + if (GetGameTime() >= g_flSpecialRoundCycleEndTime) + { + SpecialRoundCycleFinish(); + return Plugin_Stop; + } + + decl String:sBuffer[128]; + GetArrayString(g_hSpecialRoundCycleNames, g_iSpecialRoundCycleNum, sBuffer, sizeof(sBuffer)); + + GameTextTFMessage(sBuffer); + + g_iSpecialRoundCycleNum++; + if (g_iSpecialRoundCycleNum >= GetArraySize(g_hSpecialRoundCycleNames)) + { + g_iSpecialRoundCycleNum = 0; + } + + return Plugin_Continue; +} + +public Action:Timer_SpecialRoundStart(Handle:timer) +{ + if (timer != g_hSpecialRoundTimer) return; + if (!g_bSpecialRound) return; + + SpecialRoundStart(); +} + +/* +public Action:Timer_SpecialRoundAttribute(Handle:timer) +{ + if (timer != g_hSpecialRoundTimer) return Plugin_Stop; + if (!g_bSpecialRound) return Plugin_Stop; + + new iCond = -1; + + switch (g_iSpecialRoundType) + { + case SPECIALROUND_DEFENSEBUFF: iCond = _:TFCond_DefenseBuffed; + case SPECIALROUND_MARKEDFORDEATH: iCond = _:TFCond_MarkedForDeath; + } + + if (iCond != -1) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || g_bPlayerGhostMode[i]) continue; + + TF2_AddCondition(i, TFCond:iCond, 0.8); + } + } + + return Plugin_Continue; +} +*/ + +SpecialRoundCycleStart() +{ + if (!g_bSpecialRound) return; + + EmitSoundToAll(SR_MUSIC, _, MUSIC_CHAN); + g_iSpecialRoundType = 0; + g_iSpecialRoundCycleNum = 0; + g_flSpecialRoundCycleEndTime = GetGameTime() + SR_CYCLELENGTH; + g_hSpecialRoundTimer = CreateTimer(0.12, Timer_SpecialRoundCycle, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +SpecialRoundCycleFinish() +{ + EmitSoundToAll(SR_SOUND_SELECT, _, SNDCHAN_AUTO); + + new iOverride = GetConVarInt(g_cvSpecialRoundOverride); + if (iOverride >= 1 && iOverride < SPECIALROUND_MAXROUNDS) + { + g_iSpecialRoundType = iOverride; + } + else + { + new Handle:hEnabledRounds = CreateArray(); + + if (GetArraySize(GetSelectableBossProfileList()) > 0) + { + PushArrayCell(hEnabledRounds, SPECIALROUND_DOUBLETROUBLE); + } + + if (GetActivePlayerCount() <= GetConVarInt(g_cvMaxPlayers) * 2) + { + PushArrayCell(hEnabledRounds, SPECIALROUND_DOUBLEMAXPLAYERS); + } + + /* + if (GetActivePlayerCount() > 1) + { + PushArrayCell(hEnabledRounds, SPECIALROUND_SINGLEPLAYER); + } + */ + + PushArrayCell(hEnabledRounds, SPECIALROUND_INSANEDIFFICULTY); + PushArrayCell(hEnabledRounds, SPECIALROUND_LIGHTSOUT); + + g_iSpecialRoundType = GetArrayCell(hEnabledRounds, GetRandomInt(0, GetArraySize(hEnabledRounds) - 1)); + + CloseHandle(hEnabledRounds); + } + + SetConVarInt(g_cvSpecialRoundOverride, -1); + + decl String:sDescHud[64]; + SpecialRoundGetDescriptionHud(g_iSpecialRoundType, sDescHud, sizeof(sDescHud)); + + decl String:sIconHud[64]; + SpecialRoundGetIconHud(g_iSpecialRoundType, sIconHud, sizeof(sIconHud)); + + decl String:sDescChat[64]; + SpecialRoundGetDescriptionChat(g_iSpecialRoundType, sDescChat, sizeof(sDescChat)); + + GameTextTFMessage(sDescHud, sIconHud); + CPrintToChatAll("%t", "SF2 Special Round Announce Chat", sDescChat); // For those who are using minimized HUD... + + g_hSpecialRoundTimer = CreateTimer(SR_STARTDELAY, Timer_SpecialRoundStart, _, TIMER_FLAG_NO_MAPCHANGE); +} + +SpecialRoundStart() +{ + if (!g_bSpecialRound) return; + if (g_iSpecialRoundType < 1 || g_iSpecialRoundType >= SPECIALROUND_MAXROUNDS) return; + + // What to do with the timer... + switch (g_iSpecialRoundType) + { + /* + case SPECIALROUND_DEFENSEBUFF, SPECIALROUND_MARKEDFORDEATH: + { + g_hSpecialRoundTimer = CreateTimer(0.5, Timer_SpecialRoundAttribute, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + */ + default: + { + g_hSpecialRoundTimer = INVALID_HANDLE; + } + } + + switch (g_iSpecialRoundType) + { + case SPECIALROUND_DOUBLETROUBLE: + { + decl String:sBuffer[SF2_MAX_PROFILE_NAME_LENGTH]; + new Handle:hSelectableBosses = GetSelectableBossProfileList(); + + if (GetArraySize(hSelectableBosses) > 0) + { + GetArrayString(hSelectableBosses, GetRandomInt(0, GetArraySize(hSelectableBosses) - 1), sBuffer, sizeof(sBuffer)); + AddProfile(sBuffer); + } + } + case SPECIALROUND_INSANEDIFFICULTY: + { + SetConVarString(g_cvDifficulty, "3"); // Override difficulty to Insane. + } + case SPECIALROUND_SINGLEPLAYER: + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + ClientUpdateListeningFlags(i); + } + } + case SPECIALROUND_DOUBLEMAXPLAYERS: + { + ForceInNextPlayersInQueue(GetConVarInt(g_cvMaxPlayers)); + SetConVarString(g_cvDifficulty, "3"); // Override difficulty to Insane. + } + case SPECIALROUND_LIGHTSOUT: + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + ClientResetFlashlight(i); + ClientActivateUltravision(i); + } + } + } + } +} + +public Action:Timer_DisplaySpecialRound(Handle:timer) +{ + decl String:sDescHud[64]; + SpecialRoundGetDescriptionHud(g_iSpecialRoundType, sDescHud, sizeof(sDescHud)); + + decl String:sIconHud[64]; + SpecialRoundGetIconHud(g_iSpecialRoundType, sIconHud, sizeof(sIconHud)); + + decl String:sDescChat[64]; + SpecialRoundGetDescriptionChat(g_iSpecialRoundType, sDescChat, sizeof(sDescChat)); + + GameTextTFMessage(sDescHud, sIconHud); + CPrintToChatAll("%t", "SF2 Special Round Announce Chat", sDescChat); // For those who are using minimized HUD... +} + +SpecialRoundReset() +{ + g_iSpecialRoundType = 0; + g_hSpecialRoundTimer = INVALID_HANDLE; + g_iSpecialRoundCycleNum = 0; + g_flSpecialRoundCycleEndTime = -1.0; +} + +bool:IsSpecialRoundRunning() +{ + return g_bSpecialRound; +} + +public SpecialRoundInitializeAPI() +{ + CreateNative("SF2_IsSpecialRoundRunning", Native_IsSpecialRoundRunning); + CreateNative("SF2_GetSpecialRoundType", Native_GetSpecialRoundType); +} + +public Native_IsSpecialRoundRunning(Handle:plugin, numParams) +{ + return g_bSpecialRound; +} + +public Native_GetSpecialRoundType(Handle:plugin, numParams) +{ + return g_iSpecialRoundType; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/stocks.sp b/addons/sourcemod/scripting/rytp_horror/stocks.sp index 9d4dc79..cd0bb05 100644 --- a/addons/sourcemod/scripting/rytp_horror/stocks.sp +++ b/addons/sourcemod/scripting/rytp_horror/stocks.sp @@ -1,697 +1,702 @@ -#if defined _sf2_stocks_included - #endinput -#endif -#define _sf2_stocks_included - - -// Hud Element hiding flags (possibly outdated) -#define HIDEHUD_WEAPONSELECTION ( 1<<0 ) // Hide ammo count & weapon selection -#define HIDEHUD_FLASHLIGHT ( 1<<1 ) -#define HIDEHUD_ALL ( 1<<2 ) -#define HIDEHUD_HEALTH ( 1<<3 ) // Hide health & armor / suit battery -#define HIDEHUD_PLAYERDEAD ( 1<<4 ) // Hide when local player's dead -#define HIDEHUD_NEEDSUIT ( 1<<5 ) // Hide when the local player doesn't have the HEV suit -#define HIDEHUD_MISCSTATUS ( 1<<6 ) // Hide miscellaneous status elements (trains, pickup history, death notices, etc) -#define HIDEHUD_CHAT ( 1<<7 ) // Hide all communication elements (saytext, voice icon, etc) -#define HIDEHUD_CROSSHAIR ( 1<<8 ) // Hide crosshairs -#define HIDEHUD_VEHICLE_CROSSHAIR ( 1<<9 ) // Hide vehicle crosshair -#define HIDEHUD_INVEHICLE ( 1<<10 ) -#define HIDEHUD_BONUS_PROGRESS ( 1<<11 ) // Hide bonus progress display (for bonus map challenges) - -#define FFADE_IN 0x0001 // Just here so we don't pass 0 into the function -#define FFADE_OUT 0x0002 // Fade out (not in) -#define FFADE_MODULATE 0x0004 // Modulate (don't blend) -#define FFADE_STAYOUT 0x0008 // ignores the duration, stays faded out until new ScreenFade message received -#define FFADE_PURGE 0x0010 // Purges all other fades, replacing them with this one - -#define SF_FADE_IN 0x0001 // Fade in, not out -#define SF_FADE_MODULATE 0x0002 // Modulate, don't blend -#define SF_FADE_ONLYONE 0x0004 -#define SF_FADE_STAYOUT 0x0008 - -#define MAX_BUTTONS 26 - -#define FSOLID_CUSTOMRAYTEST 0x0001 -#define FSOLID_CUSTOMBOXTEST 0x0002 -#define FSOLID_NOT_SOLID 0x0004 -#define FSOLID_TRIGGER 0x0008 - -#define COLLISION_GROUP_DEBRIS 1 -#define COLLISION_GROUP_PLAYER 5 - -#define EFL_FORCE_CHECK_TRANSMIT (1 << 7) - -#define vec3_origin { 0.0, 0.0, 0.0 } - -// hull defines, mostly used for space checking. -new Float:HULL_HUMAN_MINS[3] = { -13.0, -13.0, 0.0 } -new Float:HULL_HUMAN_MAXS[3] = { 13.0, 13.0, 72.0 } - -// ========================================================== -// ENTITY FUNCTIONS -// ========================================================== - -stock bool:IsEntityClassname(iEnt, const String:classname[], bool:bCaseSensitive=true) -{ - if (!IsValidEntity(iEnt)) return false; - - decl String:sBuffer[256]; - GetEntityClassname(iEnt, sBuffer, sizeof(sBuffer)); - - return StrEqual(sBuffer, classname, bCaseSensitive); -} - -stock FindEntityByTargetname(const String:targetName[], const String:className[], bool:caseSensitive=true) -{ - new ent = -1; - while ((ent = FindEntityByClassname(ent, className)) != -1) - { - decl String:sName[64]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, targetName, caseSensitive)) - { - return ent; - } - } - - return INVALID_ENT_REFERENCE; -} - -stock Float:EntityDistanceFromEntity(ent1, ent2, bool:bSquared=false) -{ - if (!IsValidEntity(ent1) || !IsValidEntity(ent2)) return -1.0; - - decl Float:flMyPos[3], Float:flHisPos[3]; - GetEntPropVector(ent1, Prop_Data, "m_vecAbsOrigin", flMyPos); - GetEntPropVector(ent2, Prop_Data, "m_vecAbsOrigin", flHisPos); - return GetVectorDistance(flMyPos, flHisPos, bSquared); -} - -stock GetEntityOBBCenterPosition(ent, Float:flBuffer) -{ - decl Float:flPos[3], Float:flMins[3], Float:flMaxs[3]; - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); - GetEntPropVector(ent, Prop_Send, "m_vecMins", flMins); - GetEntPropVector(ent, Prop_Send, "m_vecMaxs", flMaxs); - - for (new i = 0; i < 3; i++) flBuffer[i] = flPos[i] + ((flMins[i] + flMaxs[i]) / 2.0); -} - -stock bool:IsSpaceOccupied(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) -{ - new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_VISIBLE, TraceRayDontHitEntity, entity); - new bool:bHit = TR_DidHit(hTrace); - ref = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - return bHit; -} - -stock bool:IsSpaceOccupiedIgnorePlayers(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) -{ - new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_VISIBLE, TraceRayDontHitPlayersOrEntity, entity); - new bool:bHit = TR_DidHit(hTrace); - ref = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - return bHit; -} - -stock bool:IsSpaceOccupiedPlayer(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) -{ - new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_PLAYERSOLID, TraceRayDontHitEntity, entity); - new bool:bHit = TR_DidHit(hTrace); - ref = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - return bHit; -} - -stock bool:IsSpaceOccupiedNPC(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) -{ - new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_NPCSOLID, TraceRayDontHitEntity, entity); - new bool:bHit = TR_DidHit(hTrace); - ref = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - return bHit; -} - -stock EntitySetAnimation(iEntity, const String:sAnimation[], bool:bDefaultAnimation=true, Float:flPlaybackRate=1.0) -{ - // Set m_nSequence to 0 to fix an animation glitch with HL2/GMod models. - SetEntProp(iEntity, Prop_Send, "m_nSequence", 0); - - if (bDefaultAnimation) - { - SetVariantString(sAnimation); - AcceptEntityInput(iEntity, "SetDefaultAnimation"); - } - - SetVariantString(sAnimation); - AcceptEntityInput(iEntity, "SetAnimation"); - SetVariantFloat(flPlaybackRate); - AcceptEntityInput(iEntity, "SetPlaybackRate"); -} - -// ========================================================== -// CLIENT ENTITY FUNCTIONS -// ========================================================== - -stock bool:IsClientCritBoosted(client) -{ - if (TF2_IsPlayerInCondition(client, TFCond_Kritzkrieged) || - TF2_IsPlayerInCondition(client, TFCond_HalloweenCritCandy) || - TF2_IsPlayerInCondition(client, TFCond_CritCanteen) || - TF2_IsPlayerInCondition(client, TFCond_CritOnFirstBlood) || - TF2_IsPlayerInCondition(client, TFCond_CritOnWin) || - TF2_IsPlayerInCondition(client, TFCond_CritOnFlagCapture) || - TF2_IsPlayerInCondition(client, TFCond_CritOnKill) || - TF2_IsPlayerInCondition(client, TFCond_CritOnDamage) || - TF2_IsPlayerInCondition(client, TFCond_CritMmmph)) - { - return true; - } - - new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); - if (IsValidEdict(iActiveWeapon)) - { - decl String:sNetClass[64]; - GetEntityNetClass(iActiveWeapon, sNetClass, sizeof(sNetClass)); - - if (StrEqual(sNetClass, "CTFFlameThrower")) - { - if (GetEntProp(iActiveWeapon, Prop_Send, "m_bCritFire")) return true; - - new iItemDef = GetEntProp(iActiveWeapon, Prop_Send, "m_iItemDefinitionIndex"); - if (iItemDef == 594 && TF2_IsPlayerInCondition(client, TFCond_CritMmmph)) return true; - } - else if (StrEqual(sNetClass, "CTFMinigun")) - { - if (GetEntProp(iActiveWeapon, Prop_Send, "m_bCritShot")) return true; - } - } - - return false; -} - -stock ClientSwitchToWeaponSlot(client, iSlot) -{ - new iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (iWeapon == -1) return; - - // EquipPlayerWeapon(client, iWeapon); // doesn't work with TF2 that well. - SetEntPropEnt(client, Prop_Send, "m_hActiveWeapon", iWeapon); -} - -stock ChangeClientTeamNoSuicide(client, team, bool:bRespawn=true) -{ - if (!IsClientInGame(client)) return; - - if (GetClientTeam(client) != team) - { - SetEntProp(client, Prop_Send, "m_lifeState", 2); - ChangeClientTeam(client, team); - SetEntProp(client, Prop_Send, "m_lifeState", 0); - if (bRespawn) TF2_RespawnPlayer(client); - } -} - -stock UTIL_ScreenShake(client, Float:amplitude, Float:duration, Float:frequency) -{ - new Handle:hBf = StartMessageOne("Shake", client); - if (hBf != INVALID_HANDLE) - { - BfWriteByte(hBf, 0); - BfWriteFloat(hBf, amplitude); - BfWriteFloat(hBf, frequency); - BfWriteFloat(hBf, duration); - EndMessage(); - } -} - -public UTIL_ScreenFade(client, duration, time, flags, r, g, b, a) -{ - new clients[1], Handle:bf; - clients[0] = client; - - bf = StartMessage("Fade", clients, 1); - BfWriteShort(bf, duration); - BfWriteShort(bf, time); - BfWriteShort(bf, flags); - BfWriteByte(bf, r); - BfWriteByte(bf, g); - BfWriteByte(bf, b); - BfWriteByte(bf, a); - EndMessage(); -} - -stock bool:IsValidClient(client) -{ - return bool:(client > 0 && client <= MaxClients && IsClientInGame(client)); -} - -// ========================================================== -// TF2-SPECIFIC FUNCTIONS -// ========================================================== - -stock ForceTeamWin(team) -{ - new ent = FindEntityByClassname(-1, "team_control_point_master"); - if (ent == -1) - { - ent = CreateEntityByName("team_control_point_master"); - DispatchSpawn(ent); - AcceptEntityInput(ent, "Enable"); - } - - SetVariantInt(team); - AcceptEntityInput(ent, "SetWinner"); -} - -stock GameTextTFMessage(const String:message[], const String:icon[]="") -{ - new ent = CreateEntityByName("game_text_tf"); - DispatchKeyValue(ent, "message", message); - DispatchKeyValue(ent, "display_to_team", "0"); - DispatchKeyValue(ent, "icon", icon); - DispatchSpawn(ent); - AcceptEntityInput(ent, "Display"); - AcceptEntityInput(ent, "Kill"); -} - -stock BuildAnnotationBitString(const clients[], iMaxClients) -{ - new iBitString = 1; - for (new i = 0; i < maxClients; i++) - { - new client = clients[i]; - if (!IsClientInGame(client) || !IsPlayerAlive(client)) continue; - - iBitString |= RoundFloat(Pow(2.0, float(client))); - } - - return iBitString; -} - -stock SpawnAnnotation(client, entity, const Float:pos[3], const String:message[], Float:lifetime) -{ - new Handle:event = CreateEvent("show_annotation", true); - if (event != INVALID_HANDLE) - { - new bitstring = BuildAnnotationBitString(id, pos, type, team); - if (bitstring > 1) - { - pos[2] -= 35.0; - SetEventFloat(event, "worldPosX", pos[0]); - SetEventFloat(event, "worldPosY", pos[1]); - SetEventFloat(event, "worldPosZ", pos[2]); - SetEventFloat(event, "lifetime", lifetime); - SetEventInt(event, "id", id); - SetEventString(event, "text", message); - SetEventInt(event, "visibilityBitfield", bitstring); - FireEvent(event); - KillTimer(event); - } - - } -} - -stock Float:TF2_GetClassBaseSpeed(TFClassType:class) -{ - switch (class) - { - case TFClass_Scout: - { - return 400.0; - } - case TFClass_Soldier: - { - return 240.0; - } - case TFClass_Pyro: - { - return 300.0; - } - case TFClass_DemoMan: - { - return 280.0; - } - case TFClass_Heavy: - { - return 230.0; - } - case TFClass_Engineer: - { - return 300.0; - } - case TFClass_Medic: - { - return 320.0; - } - case TFClass_Sniper: - { - return 300.0; - } - case TFClass_Spy: - { - return 300.0; - } - } - - return 0.0; -} - -stock Handle:PrepareItemHandle(String:classname[], index, level, quality, String:att[]) -{ - new Handle:hItem = TF2Items_CreateItem(OVERRIDE_ALL | FORCE_GENERATION); - TF2Items_SetClassname(hItem, classname); - TF2Items_SetItemIndex(hItem, index); - TF2Items_SetLevel(hItem, level); - TF2Items_SetQuality(hItem, quality); - - // Set attributes. - new String:atts[32][32]; - new count = ExplodeString(att, " ; ", atts, 32, 32); - if (count > 1) - { - TF2Items_SetNumAttributes(hItem, count / 2); - new i2 = 0; - for (new i = 0; i < count; i+= 2) - { - TF2Items_SetAttribute(hItem, i2, StringToInt(atts[i]), StringToFloat(atts[i+1])); - i2++; - } - } - else - { - TF2Items_SetNumAttributes(hItem, 0); - } - - return hItem; -} - -// Removes wearables such as botkillers from weapons. -stock TF2_RemoveWeaponSlotAndWearables(client, iSlot) -{ - new iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (!IsValidEntity(iWeapon)) return; - - new iWearable = INVALID_ENT_REFERENCE; - while ((iWearable = FindEntityByClassname(iWearable, "tf_wearable")) != -1) - { - new iWeaponAssociated = GetEntPropEnt(iWearable, Prop_Send, "m_hWeaponAssociatedWith"); - if (iWeaponAssociated == iWeapon) - { - AcceptEntityInput(iWearable, "Kill"); - } - } - - iWearable = INVALID_ENT_REFERENCE; - while ((iWearable = FindEntityByClassname(iWearable, "tf_wearable_vm")) != -1) - { - new iWeaponAssociated = GetEntPropEnt(iWearable, Prop_Send, "m_hWeaponAssociatedWith"); - if (iWeaponAssociated == iWeapon) - { - AcceptEntityInput(iWearable, "Kill"); - } - } - - TF2_RemoveWeaponSlot(client, iSlot); -} - -stock TE_SetupTFParticleEffect(iParticleSystemIndex, const Float:flOrigin[3], const Float:flStart[3]=NULL_VECTOR, iAttachType=0, iEntIndex=-1, iAttachmentPointIndex=0, bool:bControlPoint1=false, const Float:flControlPoint1Offset[3]=NULL_VECTOR) -{ - TE_Start("TFParticleEffect"); - TE_WriteFloat("m_vecOrigin[0]", flOrigin[0]); - TE_WriteFloat("m_vecOrigin[1]", flOrigin[1]); - TE_WriteFloat("m_vecOrigin[2]", flOrigin[2]); - TE_WriteFloat("m_vecStart[0]", flStart[0]); - TE_WriteFloat("m_vecStart[1]", flStart[1]); - TE_WriteFloat("m_vecStart[2]", flStart[2]); - TE_WriteNum("m_iParticleSystemIndex", iParticleSystemIndex); - TE_WriteNum("m_iAttachType", iAttachType); - TE_WriteNum("entindex", iEntIndex); - TE_WriteNum("m_iAttachmentPointIndex", iAttachmentPointIndex); - TE_WriteNum("m_bControlPoint1", bControlPoint1); - TE_WriteFloat("m_ControlPoint1.m_vecOffset[0]", flControlPoint1Offset[0]); - TE_WriteFloat("m_ControlPoint1.m_vecOffset[1]", flControlPoint1Offset[1]); - TE_WriteFloat("m_ControlPoint1.m_vecOffset[2]", flControlPoint1Offset[2]); -} - -// ========================================================== -// FLOAT FUNCTIONS -// ========================================================== - -/** - * Converts a given timestamp into hours, minutes, and seconds. - */ -stock FloatToTimeHMS(Float:time, &h=0, &m=0, &s=0) -{ - s = RoundFloat(time); - h = s / 3600; - s -= h * 3600; - m = s / 60; - s = s % 60; -} - -stock FixedUnsigned16(Float:value, scale) -{ - new iOutput; - - iOutput = RoundToFloor(value * float(scale)); - - if (iOutput < 0) - { - iOutput = 0; - } - - if (iOutput > 0xFFFF) - { - iOutput = 0xFFFF; - } - - return iOutput; -} - -stock Float:FloatMin(Float:a, Float:b) -{ - if (a < b) return a; - return b; -} - -stock Float:FloatMax(Float:a, Float:b) -{ - if (a > b) return a; - return b; -} - -// ========================================================== -// VECTOR FUNCTIONS -// ========================================================== - -/** - * Copies a vector into another vector. - */ -stock CopyVector(const Float:flCopy[3], Float:flDest[3]) -{ - flDest[0] = flCopy[0]; - flDest[1] = flCopy[1]; - flDest[2] = flCopy[2]; -} - -stock LerpVectors(const Float:fA[3], const Float:fB[3], Float:fC[3], Float:t) -{ - if (t < 0.0) t = 0.0; - if (t > 1.0) t = 1.0; - - fC[0] = fA[0] + (fB[0] - fA[0]) * t; - fC[1] = fA[1] + (fB[1] - fA[1]) * t; - fC[2] = fA[2] + (fB[2] - fA[2]) * t; -} - -/** - * Translates and re-orients a given offset vector into world space, given a world position and angle. - */ -stock VectorTransform(const Float:offset[3], const Float:worldpos[3], const Float:ang[3], Float:buffer[3]) -{ - decl Float:fwd[3], Float:right[3], Float:up[3]; - GetAngleVectors(ang, fwd, right, up); - - NormalizeVector(fwd, fwd); - NormalizeVector(right, right); - NormalizeVector(up, up); - - ScaleVector(right, offset[1]); - ScaleVector(fwd, offset[0]); - ScaleVector(up, offset[2]); - - buffer[0] = worldpos[0] + right[0] + fwd[0] + up[0]; - buffer[1] = worldpos[1] + right[1] + fwd[1] + up[1]; - buffer[2] = worldpos[2] + right[2] + fwd[2] + up[2]; -} - -// ========================================================== -// ANGLE FUNCTIONS -// ========================================================== - -stock Float:ApproachAngle(Float:target, Float:value, Float:speed) -{ - new Float:delta = AngleDiff(value, target); - - if (speed < 0.0) speed = -speed; - - if (delta > speed) value += speed; - else if (delta < -speed) value -= speed; - else value = target; - - return AngleNormalize(value); -} - -stock Float:AngleNormalize(Float:angle) -{ - while (angle > 180.0) angle -= 360.0; - while (angle < -180.0) angle += 360.0; - return angle; -} - -stock Float:AngleDiff(Float:firstAngle, Float:secondAngle) -{ - new Float:diff = secondAngle - firstAngle; - return AngleNormalize(diff); -} - -// ========================================================== -// PRECACHING FUNCTIONS -// ========================================================== - -stock PrecacheSound2(const String:path[]) -{ - PrecacheSound(path, true); - decl String:buffer[PLATFORM_MAX_PATH]; - Format(buffer, sizeof(buffer), "sound/%s", path); - AddFileToDownloadsTable(buffer); -} - -stock PrecacheMaterial2(const String:path[]) -{ - decl String:buffer[PLATFORM_MAX_PATH]; - Format(buffer, sizeof(buffer), "materials/%s.vmt", path); - AddFileToDownloadsTable(buffer); - Format(buffer, sizeof(buffer), "materials/%s.vtf", path); - AddFileToDownloadsTable(buffer); -} - -stock PrecacheParticleSystem(const String:particleSystem[]) -{ - static particleEffectNames = INVALID_STRING_TABLE; - - if (particleEffectNames == INVALID_STRING_TABLE) { - if ((particleEffectNames = FindStringTable("ParticleEffectNames")) == INVALID_STRING_TABLE) { - return INVALID_STRING_INDEX; - } - } - - new index = FindStringIndex2(particleEffectNames, particleSystem); - if (index == INVALID_STRING_INDEX) { - new numStrings = GetStringTableNumStrings(particleEffectNames); - if (numStrings >= GetStringTableMaxStrings(particleEffectNames)) { - return INVALID_STRING_INDEX; - } - - AddToStringTable(particleEffectNames, particleSystem); - index = numStrings; - } - - return index; -} - -stock FindStringIndex2(tableidx, const String:str[]) -{ - decl String:buf[1024]; - - new numStrings = GetStringTableNumStrings(tableidx); - for (new i=0; i < numStrings; i++) { - ReadStringTable(tableidx, i, buf, sizeof(buf)); - - if (StrEqual(buf, str)) { - return i; - } - } - - return INVALID_STRING_INDEX; -} - -stock InsertNodesAroundPoint(Handle:hArray, const Float:flOrigin[3], Float:flDist, Float:flAddAng, Function:iCallback=INVALID_FUNCTION, any:data=-1) -{ - decl Float:flDirection[3]; - decl Float:flPos[3]; - - for (new Float:flAng = 0.0; flAng < 360.0; flAng += flAddAng) - { - flDirection[0] = 0.0; - flDirection[1] = flAng; - flDirection[2] = 0.0; - - GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flDirection, flDirection); - ScaleVector(flDirection, flDist); - AddVectors(flDirection, flOrigin, flPos); - - new Float:flPos2[3]; - for (new i = 0; i < 2; i++) flPos2[i] = flPos[i]; - - if (iCallback != INVALID_FUNCTION) - { - new Action:iAction = Plugin_Continue; - - Call_StartFunction(INVALID_HANDLE, iCallback); - Call_PushArray(flOrigin, 3); - Call_PushArrayEx(flPos2, 3, SM_PARAM_COPYBACK); - Call_PushCell(data); - Call_Finish(iAction); - - if (iAction == Plugin_Stop || iAction == Plugin_Handled) continue; - else if (iAction == Plugin_Changed) - { - for (new i = 0; i < 2; i++) flPos[i] = flPos2[i]; - } - } - - PushArrayArray(hArray, flPos, 3); - } -} - -// ========================================================== -// TRACE FUNCTIONS -// ========================================================== - -public bool:TraceRayDontHitEntity(entity, mask, any:data) -{ - if (entity == data) return false; - return true; -} - -public bool:TraceRayDontHitPlayers(entity, mask, any:data) -{ - if (entity > 0 && entity <= MaxClients) return false; - - return true; -} - -public bool:TraceRayDontHitPlayersOrEntity(entity, mask, any:data) -{ - if (entity == data) return false; - - if (entity > 0 && entity <= MaxClients) return false; - - return true; -} - -// ========================================================== -// TIMER/CALLBACK FUNCTIONS -// ========================================================== - -public Action:Timer_KillEntity(Handle:timer, any:entref) -{ - new ent = EntRefToEntIndex(entref); - if (ent == INVALID_ENT_REFERENCE) return; - - AcceptEntityInput(ent, "Kill"); +#if defined _sf2_stocks_included + #endinput +#endif +#define _sf2_stocks_included + + +// Hud Element hiding flags (possibly outdated) +#define HIDEHUD_WEAPONSELECTION ( 1<<0 ) // Hide ammo count & weapon selection +#define HIDEHUD_FLASHLIGHT ( 1<<1 ) +#define HIDEHUD_ALL ( 1<<2 ) +#define HIDEHUD_HEALTH ( 1<<3 ) // Hide health & armor / suit battery +#define HIDEHUD_PLAYERDEAD ( 1<<4 ) // Hide when local player's dead +#define HIDEHUD_NEEDSUIT ( 1<<5 ) // Hide when the local player doesn't have the HEV suit +#define HIDEHUD_MISCSTATUS ( 1<<6 ) // Hide miscellaneous status elements (trains, pickup history, death notices, etc) +#define HIDEHUD_CHAT ( 1<<7 ) // Hide all communication elements (saytext, voice icon, etc) +#define HIDEHUD_CROSSHAIR ( 1<<8 ) // Hide crosshairs +#define HIDEHUD_VEHICLE_CROSSHAIR ( 1<<9 ) // Hide vehicle crosshair +#define HIDEHUD_INVEHICLE ( 1<<10 ) +#define HIDEHUD_BONUS_PROGRESS ( 1<<11 ) // Hide bonus progress display (for bonus map challenges) + +#define FFADE_IN 0x0001 // Just here so we don't pass 0 into the function +#define FFADE_OUT 0x0002 // Fade out (not in) +#define FFADE_MODULATE 0x0004 // Modulate (don't blend) +#define FFADE_STAYOUT 0x0008 // ignores the duration, stays faded out until new ScreenFade message received +#define FFADE_PURGE 0x0010 // Purges all other fades, replacing them with this one + +#define SF_FADE_IN 0x0001 // Fade in, not out +#define SF_FADE_MODULATE 0x0002 // Modulate, don't blend +#define SF_FADE_ONLYONE 0x0004 +#define SF_FADE_STAYOUT 0x0008 + +#define MAX_BUTTONS 26 + +#define FSOLID_CUSTOMRAYTEST 0x0001 +#define FSOLID_CUSTOMBOXTEST 0x0002 +#define FSOLID_NOT_SOLID 0x0004 +#define FSOLID_TRIGGER 0x0008 + +#define COLLISION_GROUP_DEBRIS 1 +#define COLLISION_GROUP_PLAYER 5 + +#define EFL_FORCE_CHECK_TRANSMIT (1 << 7) + +#define vec3_origin { 0.0, 0.0, 0.0 } + +// hull defines, mostly used for space checking. +new Float:HULL_HUMAN_MINS[3] = { -13.0, -13.0, 0.0 } +new Float:HULL_HUMAN_MAXS[3] = { 13.0, 13.0, 72.0 } + +// ========================================================== +// ENTITY FUNCTIONS +// ========================================================== + +stock bool:IsEntityClassname(iEnt, const String:classname[], bool:bCaseSensitive=true) +{ + if (!IsValidEntity(iEnt)) return false; + + decl String:sBuffer[256]; + GetEntityClassname(iEnt, sBuffer, sizeof(sBuffer)); + + return StrEqual(sBuffer, classname, bCaseSensitive); +} + +stock FindEntityByTargetname(const String:targetName[], const String:className[], bool:caseSensitive=true) +{ + new ent = -1; + while ((ent = FindEntityByClassname(ent, className)) != -1) + { + decl String:sName[64]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, targetName, caseSensitive)) + { + return ent; + } + } + + return INVALID_ENT_REFERENCE; +} + +stock Float:EntityDistanceFromEntity(ent1, ent2, bool:bSquared=false) +{ + if (!IsValidEntity(ent1) || !IsValidEntity(ent2)) return -1.0; + + decl Float:flMyPos[3], Float:flHisPos[3]; + GetEntPropVector(ent1, Prop_Data, "m_vecAbsOrigin", flMyPos); + GetEntPropVector(ent2, Prop_Data, "m_vecAbsOrigin", flHisPos); + return GetVectorDistance(flMyPos, flHisPos, bSquared); +} + +stock GetEntityOBBCenterPosition(ent, Float:flBuffer) +{ + decl Float:flPos[3], Float:flMins[3], Float:flMaxs[3]; + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); + GetEntPropVector(ent, Prop_Send, "m_vecMins", flMins); + GetEntPropVector(ent, Prop_Send, "m_vecMaxs", flMaxs); + + for (new i = 0; i < 3; i++) flBuffer[i] = flPos[i] + ((flMins[i] + flMaxs[i]) / 2.0); +} + +stock bool:IsSpaceOccupied(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) +{ + new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_VISIBLE, TraceRayDontHitEntity, entity); + new bool:bHit = TR_DidHit(hTrace); + ref = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + return bHit; +} + +stock bool:IsSpaceOccupiedIgnorePlayers(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) +{ + new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_VISIBLE, TraceRayDontHitPlayersOrEntity, entity); + new bool:bHit = TR_DidHit(hTrace); + ref = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + return bHit; +} + +stock bool:IsSpaceOccupiedPlayer(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) +{ + new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_PLAYERSOLID, TraceRayDontHitEntity, entity); + new bool:bHit = TR_DidHit(hTrace); + ref = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + return bHit; +} + +stock bool:IsSpaceOccupiedNPC(const Float:pos[3], const Float:mins[3], const Float:maxs[3], entity=-1, &ref=-1) +{ + new Handle:hTrace = TR_TraceHullFilterEx(pos, pos, mins, maxs, MASK_NPCSOLID, TraceRayDontHitEntity, entity); + new bool:bHit = TR_DidHit(hTrace); + ref = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + return bHit; +} + +stock EntitySetAnimation(iEntity, const String:sAnimation[], bool:bDefaultAnimation=true, Float:flPlaybackRate=1.0) +{ + // Set m_nSequence to 0 to fix an animation glitch with HL2/GMod models. + SetEntProp(iEntity, Prop_Send, "m_nSequence", 0); + + if (bDefaultAnimation) + { + SetVariantString(sAnimation); + AcceptEntityInput(iEntity, "SetDefaultAnimation"); + } + else + { + SetVariantString(""); + AcceptEntityInput(iEntity, "SetDefaultAnimation"); + } + + SetVariantString(sAnimation); + AcceptEntityInput(iEntity, "SetAnimation"); + SetVariantFloat(flPlaybackRate); + AcceptEntityInput(iEntity, "SetPlaybackRate"); +} + +// ========================================================== +// CLIENT ENTITY FUNCTIONS +// ========================================================== + +stock bool:IsClientCritBoosted(client) +{ + if (TF2_IsPlayerInCondition(client, TFCond_Kritzkrieged) || + TF2_IsPlayerInCondition(client, TFCond_HalloweenCritCandy) || + TF2_IsPlayerInCondition(client, TFCond_CritCanteen) || + TF2_IsPlayerInCondition(client, TFCond_CritOnFirstBlood) || + TF2_IsPlayerInCondition(client, TFCond_CritOnWin) || + TF2_IsPlayerInCondition(client, TFCond_CritOnFlagCapture) || + TF2_IsPlayerInCondition(client, TFCond_CritOnKill) || + TF2_IsPlayerInCondition(client, TFCond_CritOnDamage) || + TF2_IsPlayerInCondition(client, TFCond_CritMmmph)) + { + return true; + } + + new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + if (IsValidEdict(iActiveWeapon)) + { + decl String:sNetClass[64]; + GetEntityNetClass(iActiveWeapon, sNetClass, sizeof(sNetClass)); + + if (StrEqual(sNetClass, "CTFFlameThrower")) + { + if (GetEntProp(iActiveWeapon, Prop_Send, "m_bCritFire")) return true; + + new iItemDef = GetEntProp(iActiveWeapon, Prop_Send, "m_iItemDefinitionIndex"); + if (iItemDef == 594 && TF2_IsPlayerInCondition(client, TFCond_CritMmmph)) return true; + } + else if (StrEqual(sNetClass, "CTFMinigun")) + { + if (GetEntProp(iActiveWeapon, Prop_Send, "m_bCritShot")) return true; + } + } + + return false; +} + +stock ClientSwitchToWeaponSlot(client, iSlot) +{ + new iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (iWeapon == -1) return; + + // EquipPlayerWeapon(client, iWeapon); // doesn't work with TF2 that well. + SetEntPropEnt(client, Prop_Send, "m_hActiveWeapon", iWeapon); +} + +stock ChangeClientTeamNoSuicide(client, team, bool:bRespawn=true) +{ + if (!IsClientInGame(client)) return; + + if (GetClientTeam(client) != team) + { + SetEntProp(client, Prop_Send, "m_lifeState", 2); + ChangeClientTeam(client, team); + SetEntProp(client, Prop_Send, "m_lifeState", 0); + if (bRespawn) TF2_RespawnPlayer(client); + } +} + +stock UTIL_ScreenShake(client, Float:amplitude, Float:duration, Float:frequency) +{ + new Handle:hBf = StartMessageOne("Shake", client); + if (hBf != INVALID_HANDLE) + { + BfWriteByte(hBf, 0); + BfWriteFloat(hBf, amplitude); + BfWriteFloat(hBf, frequency); + BfWriteFloat(hBf, duration); + EndMessage(); + } +} + +public UTIL_ScreenFade(client, duration, time, flags, r, g, b, a) +{ + new clients[1], Handle:bf; + clients[0] = client; + + bf = StartMessage("Fade", clients, 1); + BfWriteShort(bf, duration); + BfWriteShort(bf, time); + BfWriteShort(bf, flags); + BfWriteByte(bf, r); + BfWriteByte(bf, g); + BfWriteByte(bf, b); + BfWriteByte(bf, a); + EndMessage(); +} + +stock bool:IsValidClient(client) +{ + return bool:(client > 0 && client <= MaxClients && IsClientInGame(client)); +} + +// ========================================================== +// TF2-SPECIFIC FUNCTIONS +// ========================================================== + +stock ForceTeamWin(team) +{ + new ent = FindEntityByClassname(-1, "team_control_point_master"); + if (ent == -1) + { + ent = CreateEntityByName("team_control_point_master"); + DispatchSpawn(ent); + AcceptEntityInput(ent, "Enable"); + } + + SetVariantInt(team); + AcceptEntityInput(ent, "SetWinner"); +} + +stock GameTextTFMessage(const String:message[], const String:icon[]="") +{ + new ent = CreateEntityByName("game_text_tf"); + DispatchKeyValue(ent, "message", message); + DispatchKeyValue(ent, "display_to_team", "0"); + DispatchKeyValue(ent, "icon", icon); + DispatchSpawn(ent); + AcceptEntityInput(ent, "Display"); + AcceptEntityInput(ent, "Kill"); +} + +stock BuildAnnotationBitString(const clients[], iMaxClients) +{ + new iBitString = 1; + for (new i = 0; i < maxClients; i++) + { + new client = clients[i]; + if (!IsClientInGame(client) || !IsPlayerAlive(client)) continue; + + iBitString |= RoundFloat(Pow(2.0, float(client))); + } + + return iBitString; +} + +stock SpawnAnnotation(client, entity, const Float:pos[3], const String:message[], Float:lifetime) +{ + new Handle:event = CreateEvent("show_annotation", true); + if (event != INVALID_HANDLE) + { + new bitstring = BuildAnnotationBitString(id, pos, type, team); + if (bitstring > 1) + { + pos[2] -= 35.0; + SetEventFloat(event, "worldPosX", pos[0]); + SetEventFloat(event, "worldPosY", pos[1]); + SetEventFloat(event, "worldPosZ", pos[2]); + SetEventFloat(event, "lifetime", lifetime); + SetEventInt(event, "id", id); + SetEventString(event, "text", message); + SetEventInt(event, "visibilityBitfield", bitstring); + FireEvent(event); + KillTimer(event); + } + + } +} + +stock Float:TF2_GetClassBaseSpeed(TFClassType:class) +{ + switch (class) + { + case TFClass_Scout: + { + return 400.0; + } + case TFClass_Soldier: + { + return 240.0; + } + case TFClass_Pyro: + { + return 300.0; + } + case TFClass_DemoMan: + { + return 280.0; + } + case TFClass_Heavy: + { + return 230.0; + } + case TFClass_Engineer: + { + return 300.0; + } + case TFClass_Medic: + { + return 320.0; + } + case TFClass_Sniper: + { + return 300.0; + } + case TFClass_Spy: + { + return 300.0; + } + } + + return 0.0; +} + +stock Handle:PrepareItemHandle(String:classname[], index, level, quality, String:att[]) +{ + new Handle:hItem = TF2Items_CreateItem(OVERRIDE_ALL | FORCE_GENERATION); + TF2Items_SetClassname(hItem, classname); + TF2Items_SetItemIndex(hItem, index); + TF2Items_SetLevel(hItem, level); + TF2Items_SetQuality(hItem, quality); + + // Set attributes. + new String:atts[32][32]; + new count = ExplodeString(att, " ; ", atts, 32, 32); + if (count > 1) + { + TF2Items_SetNumAttributes(hItem, count / 2); + new i2 = 0; + for (new i = 0; i < count; i+= 2) + { + TF2Items_SetAttribute(hItem, i2, StringToInt(atts[i]), StringToFloat(atts[i+1])); + i2++; + } + } + else + { + TF2Items_SetNumAttributes(hItem, 0); + } + + return hItem; +} + +// Removes wearables such as botkillers from weapons. +stock TF2_RemoveWeaponSlotAndWearables(client, iSlot) +{ + new iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (!IsValidEntity(iWeapon)) return; + + new iWearable = INVALID_ENT_REFERENCE; + while ((iWearable = FindEntityByClassname(iWearable, "tf_wearable")) != -1) + { + new iWeaponAssociated = GetEntPropEnt(iWearable, Prop_Send, "m_hWeaponAssociatedWith"); + if (iWeaponAssociated == iWeapon) + { + AcceptEntityInput(iWearable, "Kill"); + } + } + + iWearable = INVALID_ENT_REFERENCE; + while ((iWearable = FindEntityByClassname(iWearable, "tf_wearable_vm")) != -1) + { + new iWeaponAssociated = GetEntPropEnt(iWearable, Prop_Send, "m_hWeaponAssociatedWith"); + if (iWeaponAssociated == iWeapon) + { + AcceptEntityInput(iWearable, "Kill"); + } + } + + TF2_RemoveWeaponSlot(client, iSlot); +} + +stock TE_SetupTFParticleEffect(iParticleSystemIndex, const Float:flOrigin[3], const Float:flStart[3]=NULL_VECTOR, iAttachType=0, iEntIndex=-1, iAttachmentPointIndex=0, bool:bControlPoint1=false, const Float:flControlPoint1Offset[3]=NULL_VECTOR) +{ + TE_Start("TFParticleEffect"); + TE_WriteFloat("m_vecOrigin[0]", flOrigin[0]); + TE_WriteFloat("m_vecOrigin[1]", flOrigin[1]); + TE_WriteFloat("m_vecOrigin[2]", flOrigin[2]); + TE_WriteFloat("m_vecStart[0]", flStart[0]); + TE_WriteFloat("m_vecStart[1]", flStart[1]); + TE_WriteFloat("m_vecStart[2]", flStart[2]); + TE_WriteNum("m_iParticleSystemIndex", iParticleSystemIndex); + TE_WriteNum("m_iAttachType", iAttachType); + TE_WriteNum("entindex", iEntIndex); + TE_WriteNum("m_iAttachmentPointIndex", iAttachmentPointIndex); + TE_WriteNum("m_bControlPoint1", bControlPoint1); + TE_WriteFloat("m_ControlPoint1.m_vecOffset[0]", flControlPoint1Offset[0]); + TE_WriteFloat("m_ControlPoint1.m_vecOffset[1]", flControlPoint1Offset[1]); + TE_WriteFloat("m_ControlPoint1.m_vecOffset[2]", flControlPoint1Offset[2]); +} + +// ========================================================== +// FLOAT FUNCTIONS +// ========================================================== + +/** + * Converts a given timestamp into hours, minutes, and seconds. + */ +stock FloatToTimeHMS(Float:time, &h=0, &m=0, &s=0) +{ + s = RoundFloat(time); + h = s / 3600; + s -= h * 3600; + m = s / 60; + s = s % 60; +} + +stock FixedUnsigned16(Float:value, scale) +{ + new iOutput; + + iOutput = RoundToFloor(value * float(scale)); + + if (iOutput < 0) + { + iOutput = 0; + } + + if (iOutput > 0xFFFF) + { + iOutput = 0xFFFF; + } + + return iOutput; +} + +stock Float:FloatMin(Float:a, Float:b) +{ + if (a < b) return a; + return b; +} + +stock Float:FloatMax(Float:a, Float:b) +{ + if (a > b) return a; + return b; +} + +// ========================================================== +// VECTOR FUNCTIONS +// ========================================================== + +/** + * Copies a vector into another vector. + */ +stock CopyVector(const Float:flCopy[3], Float:flDest[3]) +{ + flDest[0] = flCopy[0]; + flDest[1] = flCopy[1]; + flDest[2] = flCopy[2]; +} + +stock LerpVectors(const Float:fA[3], const Float:fB[3], Float:fC[3], Float:t) +{ + if (t < 0.0) t = 0.0; + if (t > 1.0) t = 1.0; + + fC[0] = fA[0] + (fB[0] - fA[0]) * t; + fC[1] = fA[1] + (fB[1] - fA[1]) * t; + fC[2] = fA[2] + (fB[2] - fA[2]) * t; +} + +/** + * Translates and re-orients a given offset vector into world space, given a world position and angle. + */ +stock VectorTransform(const Float:offset[3], const Float:worldpos[3], const Float:ang[3], Float:buffer[3]) +{ + decl Float:fwd[3], Float:right[3], Float:up[3]; + GetAngleVectors(ang, fwd, right, up); + + NormalizeVector(fwd, fwd); + NormalizeVector(right, right); + NormalizeVector(up, up); + + ScaleVector(right, offset[1]); + ScaleVector(fwd, offset[0]); + ScaleVector(up, offset[2]); + + buffer[0] = worldpos[0] + right[0] + fwd[0] + up[0]; + buffer[1] = worldpos[1] + right[1] + fwd[1] + up[1]; + buffer[2] = worldpos[2] + right[2] + fwd[2] + up[2]; +} + +// ========================================================== +// ANGLE FUNCTIONS +// ========================================================== + +stock Float:ApproachAngle(Float:target, Float:value, Float:speed) +{ + new Float:delta = AngleDiff(value, target); + + if (speed < 0.0) speed = -speed; + + if (delta > speed) value += speed; + else if (delta < -speed) value -= speed; + else value = target; + + return AngleNormalize(value); +} + +stock Float:AngleNormalize(Float:angle) +{ + while (angle > 180.0) angle -= 360.0; + while (angle < -180.0) angle += 360.0; + return angle; +} + +stock Float:AngleDiff(Float:firstAngle, Float:secondAngle) +{ + new Float:diff = secondAngle - firstAngle; + return AngleNormalize(diff); +} + +// ========================================================== +// PRECACHING FUNCTIONS +// ========================================================== + +stock PrecacheSound2(const String:path[]) +{ + PrecacheSound(path, true); + decl String:buffer[PLATFORM_MAX_PATH]; + Format(buffer, sizeof(buffer), "sound/%s", path); + AddFileToDownloadsTable(buffer); +} + +stock PrecacheMaterial2(const String:path[]) +{ + decl String:buffer[PLATFORM_MAX_PATH]; + Format(buffer, sizeof(buffer), "materials/%s.vmt", path); + AddFileToDownloadsTable(buffer); + Format(buffer, sizeof(buffer), "materials/%s.vtf", path); + AddFileToDownloadsTable(buffer); +} + +stock PrecacheParticleSystem(const String:particleSystem[]) +{ + static particleEffectNames = INVALID_STRING_TABLE; + + if (particleEffectNames == INVALID_STRING_TABLE) { + if ((particleEffectNames = FindStringTable("ParticleEffectNames")) == INVALID_STRING_TABLE) { + return INVALID_STRING_INDEX; + } + } + + new index = FindStringIndex2(particleEffectNames, particleSystem); + if (index == INVALID_STRING_INDEX) { + new numStrings = GetStringTableNumStrings(particleEffectNames); + if (numStrings >= GetStringTableMaxStrings(particleEffectNames)) { + return INVALID_STRING_INDEX; + } + + AddToStringTable(particleEffectNames, particleSystem); + index = numStrings; + } + + return index; +} + +stock FindStringIndex2(tableidx, const String:str[]) +{ + decl String:buf[1024]; + + new numStrings = GetStringTableNumStrings(tableidx); + for (new i=0; i < numStrings; i++) { + ReadStringTable(tableidx, i, buf, sizeof(buf)); + + if (StrEqual(buf, str)) { + return i; + } + } + + return INVALID_STRING_INDEX; +} + +stock InsertNodesAroundPoint(Handle:hArray, const Float:flOrigin[3], Float:flDist, Float:flAddAng, Function:iCallback=INVALID_FUNCTION, any:data=-1) +{ + decl Float:flDirection[3]; + decl Float:flPos[3]; + + for (new Float:flAng = 0.0; flAng < 360.0; flAng += flAddAng) + { + flDirection[0] = 0.0; + flDirection[1] = flAng; + flDirection[2] = 0.0; + + GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flDirection, flDirection); + ScaleVector(flDirection, flDist); + AddVectors(flDirection, flOrigin, flPos); + + new Float:flPos2[3]; + for (new i = 0; i < 2; i++) flPos2[i] = flPos[i]; + + if (iCallback != INVALID_FUNCTION) + { + new Action:iAction = Plugin_Continue; + + Call_StartFunction(INVALID_HANDLE, iCallback); + Call_PushArray(flOrigin, 3); + Call_PushArrayEx(flPos2, 3, SM_PARAM_COPYBACK); + Call_PushCell(data); + Call_Finish(iAction); + + if (iAction == Plugin_Stop || iAction == Plugin_Handled) continue; + else if (iAction == Plugin_Changed) + { + for (new i = 0; i < 2; i++) flPos[i] = flPos2[i]; + } + } + + PushArrayArray(hArray, flPos, 3); + } +} + +// ========================================================== +// TRACE FUNCTIONS +// ========================================================== + +public bool:TraceRayDontHitEntity(entity, mask, any:data) +{ + if (entity == data) return false; + return true; +} + +public bool:TraceRayDontHitPlayers(entity, mask, any:data) +{ + if (entity > 0 && entity <= MaxClients) return false; + + return true; +} + +public bool:TraceRayDontHitPlayersOrEntity(entity, mask, any:data) +{ + if (entity == data) return false; + + if (entity > 0 && entity <= MaxClients) return false; + + return true; +} + +// ========================================================== +// TIMER/CALLBACK FUNCTIONS +// ========================================================== + +public Action:Timer_KillEntity(Handle:timer, any:entref) +{ + new ent = EntRefToEntIndex(entref); + if (ent == INVALID_ENT_REFERENCE) return; + + AcceptEntityInput(ent, "Kill"); } \ No newline at end of file diff --git a/addons/sourcemod/scripting/spcomp.exe b/addons/sourcemod/scripting/spcomp.exe index 0da62a81aff47bb6d4d52ede151def06c0dc6a48..fe100b61e6cef038060fb7f0c318da6433051909 100644 GIT binary patch literal 403968 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 AfB*mh literal 345600 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⋘bVbkC@7-lQ1EGf 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∨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≈wPNbkET_VmZKPS|4mC;@ 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 J{*S$Ze+3K?e=-07 diff --git a/addons/sourcemod/translations/ru/sf2.phrases.txt b/addons/sourcemod/translations/ru/sf2.phrases.txt index 29d7bef..59a44b4 100644 --- a/addons/sourcemod/translations/ru/sf2.phrases.txt +++ b/addons/sourcemod/translations/ru/sf2.phrases.txt @@ -316,11 +316,21 @@ "ru" "Вкл беззвучный режим" } + "SF2 Settings Film Grain Menu Title" + { + "ru" "Вкл/выкл зерниÑтоÑть" + } + "SF2 Settings Proxy Menu Title" { "ru" "Разрешить быть выбранным в качеÑтве ПрокÑи" } + "SF2 Settings Ghost Overlay Menu Title" + { + "ru" "Вкл/выкл Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ñ€Ð¸Ð·Ñ€Ð°ÐºÐ°" + } + "SF2 Ad Message 1" { "ru" "{lightgreen}RYTP Horror {olive}- проект {lightgreen}Penek-Gaming.Ru{olive}. Узнать больше об Ñтом и других режимах можно на нашем Ñайте: {lightgreen}http://www.penek-gaming.ru/tf{default}" @@ -372,6 +382,26 @@ "ru" "{lightgreen}Выключены подÑказки." } + "SF2 Enabled Film Grain" + { + "ru" "{lightgreen}ЗерниÑтоÑть включена." + } + + "SF2 Disabled Film Grain" + { + "ru" "{lightgreen}ЗерниÑтоÑть выключена." + } + + "SF2 Enabled Ghost Overlay" + { + "ru" "{lightgreen}Ð˜Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ñ€Ð¸Ð·Ñ€Ð°ÐºÐ° включен." + } + + "SF2 Disabled Ghost Overlay" + { + "ru" "{lightgreen}Ð˜Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ñ€Ð¸Ð·Ñ€Ð°ÐºÐ° выключен." + } + "SF2 Enabled Proxy" { "ru" "{lightgreen}Ð’Ñ‹ будете выбиратьÑÑ Ð² качеÑтве ПрокÑи." diff --git a/addons/sourcemod/translations/sf2.phrases.txt b/addons/sourcemod/translations/sf2.phrases.txt index 6799cca..f696075 100644 --- a/addons/sourcemod/translations/sf2.phrases.txt +++ b/addons/sourcemod/translations/sf2.phrases.txt @@ -316,11 +316,21 @@ "en" "Set mute mode" } + "SF2 Settings Film Grain Menu Title" + { + "en" "Toggle film grain" + } + "SF2 Settings Proxy Menu Title" { "en" "Allow being picked as a proxy" } + "SF2 Settings Ghost Overlay Menu Title" + { + "en" "Toggle Ghost Mode overlay" + } + "SF2 Ad Message 1" { "en" "{olive}Slender Fortress official Steam group: {lightgreen}http://steamcommunity.com/groups/SlenderFortress{default}" @@ -367,6 +377,26 @@ "en" "{lightgreen}Disabled hints." } + "SF2 Enabled Film Grain" + { + "en" "{lightgreen}Enabled film grain." + } + + "SF2 Disabled Film Grain" + { + "en" "{lightgreen}Disabled film grain." + } + + "SF2 Enabled Ghost Overlay" + { + "en" "{lightgreen}Enabled Ghost Mode overlay." + } + + "SF2 Disabled Ghost Overlay" + { + "en" "{lightgreen}Disabled Ghost Mode overlay." + } + "SF2 Enabled Proxy" { "en" "{lightgreen}You are now opt'd in to be chosen as a Proxy." -- GitLab From d35eebb382a9f721440510b4f1fa48e84034e212 Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 02:17:35 +0300 Subject: [PATCH 2/6] Migrate previously implemented features --- addons/sourcemod/scripting/rytp_horror.sp | 12901 ++++++++-------- .../sourcemod/scripting/rytp_horror/client.sp | 11811 +++++++------- 2 files changed, 12450 insertions(+), 12262 deletions(-) diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index b26e668..0ad5e9c 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -1,6376 +1,6527 @@ -#include <sourcemod> -#include <sdktools> -#include <sdkhooks> -#include <clientprefs> -#include <steamtools> -#include <tf2items> -#include <dhooks> -#include <navmesh> - -#include <tf2> -#include <tf2_stocks> -#include <morecolors> -#include <sf2> - -#undef REQUIRE_PLUGIN -#include <adminmenu> -#tryinclude <store/store-tf2footprints> -#define REQUIRE_PLUGIN - -//#define DEBUG - -// If compiling with SM 1.7+, uncomment to compile and use SF2 methodmaps. -//#define METHODMAPS - -#define PLUGIN_VERSION "0.2.6-git136" -#define PLUGIN_VERSION_DISPLAY "0.2.6" - -public Plugin:myinfo = -{ - name = "RYTP Horror (Slender Fortress edit by lexuzieel special for Penek-Gaming.Ru)", - author = "KitRifty", - description = "Based on the game Slender: The Eight Pages.", - version = PLUGIN_VERSION, - url = "http://steamcommunity.com/groups/SlenderFortress" -} - -#define FILE_RESTRICTEDWEAPONS "configs/sf2/restrictedweapons.cfg" - -#define BOSS_THINKRATE 0.1 // doesn't really matter much since timers go at a minimum of 0.1 seconds anyways - -#define CRIT_SOUND "player/crit_hit.wav" -#define CRIT_PARTICLENAME "crit_text" - -#define PAGE_MODEL "models/rytp/horror/props/hint_paper.mdl" -#define PAGE_MODELSCALE 1.1 - -#define FLASHLIGHT_CLICKSOUND "rytp_horror/toggleflashlight.wav" -#define FLASHLIGHT_BREAKSOUND "ambient/energy/spark6.wav" -#define FLASHLIGHT_NOSOUND "player/suit_denydevice.wav" -#define PAGE_GRABSOUND "rytp_horror/grabpage_sound.wav" - -#define MUSIC_CHAN SNDCHAN_AUTO - -#define MUSIC_GOTPAGES1_SOUND "rytp_horror/grabpage_music_1.wav" -#define MUSIC_GOTPAGES2_SOUND "rytp_horror/grabpage_music_2.wav" -#define MUSIC_GOTPAGES3_SOUND "rytp_horror/grabpage_music_3.wav" -#define MUSIC_GOTPAGES4_SOUND "rytp_horror/grabpage_music_4.wav" -#define MUSIC_PAGE_VOLUME 1.0 - -#define SF2_INTRO_DEFAULT_MUSIC "rytp_horror/intro_music.mp3" - -#define SF2_HUD_TEXT_COLOR_R 127 -#define SF2_HUD_TEXT_COLOR_G 167 -#define SF2_HUD_TEXT_COLOR_B 141 -#define SF2_HUD_TEXT_COLOR_A 255 - -enum MuteMode -{ - MuteMode_Normal = 0, - MuteMode_DontHearOtherTeam, - MuteMode_DontHearOtherTeamIfNotProxy -}; - -// Offsets. -new g_offsPlayerFOV = -1; -new g_offsPlayerDefaultFOV = -1; -new g_offsPlayerFogCtrl = -1; -new g_offsPlayerPunchAngle = -1; -new g_offsPlayerPunchAngleVel = -1; -new g_offsFogCtrlEnable = -1; -new g_offsFogCtrlEnd = -1; - -new g_iParticleCriticalHit = -1; - -new bool:g_bEnabled; - -new Handle:g_hConfig; -new Handle:g_hRestrictedWeaponsConfig; -new Handle:g_hSpecialRoundsConfig; - -new Handle:g_hPageMusicRanges; - -new g_iSlenderModel[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new g_iSlenderPoseEnt[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new g_iSlenderCopyMaster[MAX_BOSSES] = { -1, ... }; -new Float:g_flSlenderEyePosOffset[MAX_BOSSES][3]; -new Float:g_flSlenderEyeAngOffset[MAX_BOSSES][3]; -new Float:g_flSlenderDetectMins[MAX_BOSSES][3]; -new Float:g_flSlenderDetectMaxs[MAX_BOSSES][3]; -new Handle:g_hSlenderThink[MAX_BOSSES]; -new Handle:g_hSlenderEntityThink[MAX_BOSSES]; -new Handle:g_hSlenderFakeTimer[MAX_BOSSES]; -new Float:g_flSlenderLastKill[MAX_BOSSES]; -new g_iSlenderState[MAX_BOSSES]; -new g_iSlenderTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; -new Float:g_flSlenderAcceleration[MAX_BOSSES]; -new Float:g_flSlenderGoalPos[MAX_BOSSES][3]; -new Float:g_flSlenderStaticRadius[MAX_BOSSES]; -new Float:g_flSlenderChaseDeathPosition[MAX_BOSSES][3]; -new bool:g_bSlenderChaseDeathPosition[MAX_BOSSES]; -new Float:g_flSlenderIdleAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderWalkAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderRunAnimationPlaybackRate[MAX_BOSSES]; -new Float:g_flSlenderJumpSpeed[MAX_BOSSES]; -new Float:g_flSlenderPathNodeTolerance[MAX_BOSSES]; -new Float:g_flSlenderPathNodeLookAhead[MAX_BOSSES]; -new bool:g_bSlenderFeelerReflexAdjustment[MAX_BOSSES]; -new Float:g_flSlenderFeelerReflexAdjustmentPos[MAX_BOSSES][3]; - -new g_iSlenderTeleportTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; - -new Float:g_flSlenderNextTeleportTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportTargetTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMinRange[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxRange[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxTargetTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTeleportMaxTargetStress[MAX_BOSSES] = { 0.0, ... }; -new Float:g_flSlenderTeleportPlayersRestTime[MAX_BOSSES][MAXPLAYERS + 1]; - -// For boss type 2 -// General variables -new g_iSlenderHealth[MAX_BOSSES]; -new Handle:g_hSlenderPath[MAX_BOSSES]; -new g_iSlenderCurrentPathNode[MAX_BOSSES] = { -1, ... }; -new bool:g_bSlenderAttacking[MAX_BOSSES]; -new Handle:g_hSlenderAttackTimer[MAX_BOSSES]; -new Float:g_flSlenderNextJump[MAX_BOSSES] = { -1.0, ... }; -new g_iSlenderInterruptConditions[MAX_BOSSES]; -new Float:g_flSlenderLastFoundPlayer[MAX_BOSSES][MAXPLAYERS + 1]; -new Float:g_flSlenderLastFoundPlayerPos[MAX_BOSSES][MAXPLAYERS + 1][3]; -new Float:g_flSlenderNextPathTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderCalculatedWalkSpeed[MAX_BOSSES]; -new Float:g_flSlenderCalculatedSpeed[MAX_BOSSES]; -new Float:g_flSlenderTimeUntilNoPersistence[MAX_BOSSES]; - -new Float:g_flSlenderProxyTeleportMinRange[MAX_BOSSES]; -new Float:g_flSlenderProxyTeleportMaxRange[MAX_BOSSES]; - -// Sound variables -new Float:g_flSlenderTargetSoundLastTime[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTargetSoundMasterPos[MAX_BOSSES][3]; // to determine hearing focus -new Float:g_flSlenderTargetSoundTempPos[MAX_BOSSES][3]; -new Float:g_flSlenderTargetSoundDiscardMasterPosTime[MAX_BOSSES]; -new bool:g_bSlenderInvestigatingSound[MAX_BOSSES]; -new SoundType:g_iSlenderTargetSoundType[MAX_BOSSES] = { SoundType_None, ... }; -new g_iSlenderTargetSoundCount[MAX_BOSSES]; -new Float:g_flSlenderLastHeardVoice[MAX_BOSSES]; -new Float:g_flSlenderLastHeardFootstep[MAX_BOSSES]; -new Float:g_flSlenderLastHeardWeapon[MAX_BOSSES]; - - -new Float:g_flSlenderNextJumpScare[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextVoiceSound[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextMoanSound[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderNextWanderPos[MAX_BOSSES] = { -1.0, ... }; - - -new Float:g_flSlenderTimeUntilRecover[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilAlert[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilIdle[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilChase[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilKill[MAX_BOSSES] = { -1.0, ... }; -new Float:g_flSlenderTimeUntilNextProxy[MAX_BOSSES] = { -1.0, ... }; - -// Page data. -new g_iPageCount; -new g_iPageMax; -new Float:g_flPageFoundLastTime; -new bool:g_bPageRef; -new String:g_strPageRefModel[PLATFORM_MAX_PATH]; -new Float:g_flPageRefModelScale; - -static Handle:g_hPlayerIntroMusicTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Seeing Mr. Slendy data. -new bool:g_bPlayerSeesSlender[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerSeesSlenderLastTime[MAXPLAYERS + 1][MAX_BOSSES]; - -new Float:g_flPlayerSightSoundNextTime[MAXPLAYERS + 1][MAX_BOSSES]; - -new Float:g_flPlayerScareLastTime[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerScareNextTime[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerStaticAmount[MAXPLAYERS + 1]; - -new Float:g_flPlayerLastChaseBossEncounterTime[MAXPLAYERS + 1][MAX_BOSSES]; - -// Player static data. -new g_iPlayerStaticMode[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerStaticIncreaseRate[MAXPLAYERS + 1]; -new Float:g_flPlayerStaticDecreaseRate[MAXPLAYERS + 1]; -new Handle:g_hPlayerStaticTimer[MAXPLAYERS + 1]; -new g_iPlayerStaticMaster[MAXPLAYERS + 1] = { -1, ... }; -new String:g_strPlayerStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new String:g_strPlayerLastStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerLastStaticTime[MAXPLAYERS + 1]; -new Float:g_flPlayerLastStaticVolume[MAXPLAYERS + 1]; -new Handle:g_hPlayerLastStaticTimer[MAXPLAYERS + 1]; - -// Static shake data. -new g_iPlayerStaticShakeMaster[MAXPLAYERS + 1]; -new bool:g_bPlayerInStaticShake[MAXPLAYERS + 1]; -new String:g_strPlayerStaticShakeSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerStaticShakeMinVolume[MAXPLAYERS + 1]; -new Float:g_flPlayerStaticShakeMaxVolume[MAXPLAYERS + 1]; - -// Fake lag compensation for FF. -new bool:g_bPlayerLagCompensation[MAXPLAYERS + 1]; -new g_iPlayerLagCompensationTeam[MAXPLAYERS + 1]; - -// Hint data. -enum -{ - PlayerHint_Sprint = 0, - PlayerHint_Flashlight, - PlayerHint_MainMenu, - PlayerHint_Blink, - PlayerHint_MaxNum -}; - -enum PlayerPreferences -{ - bool:PlayerPreference_PvPAutoSpawn, - MuteMode:PlayerPreference_MuteMode, - bool:PlayerPreference_FilmGrain, - bool:PlayerPreference_ShowHints, - bool:PlayerPreference_EnableProxySelection, - bool:PlayerPreference_ProjectedFlashlight, - bool:PlayerPreference_GhostOverlay -}; - -new bool:g_bPlayerHints[MAXPLAYERS + 1][PlayerHint_MaxNum]; -new g_iPlayerPreferences[MAXPLAYERS + 1][PlayerPreferences]; - -// Player data. -new g_iPlayerLastButtons[MAXPLAYERS + 1]; -new bool:g_bPlayerChoseTeam[MAXPLAYERS + 1]; -new bool:g_bPlayerEliminated[MAXPLAYERS + 1]; -new bool:g_bPlayerEscaped[MAXPLAYERS + 1]; -new g_iPlayerPageCount[MAXPLAYERS + 1]; -new g_iPlayerQueuePoints[MAXPLAYERS + 1]; -new bool:g_bPlayerPlaying[MAXPLAYERS + 1]; -new Handle:g_hPlayerOverlayCheck[MAXPLAYERS + 1]; - -new Handle:g_hPlayerSwitchBlueTimer[MAXPLAYERS + 1]; - -// Player stress data. -new Float:g_flPlayerStress[MAXPLAYERS + 1]; -new Float:g_flPlayerStressNextUpdateTime[MAXPLAYERS + 1]; - -// Proxy data. -new bool:g_bPlayerProxy[MAXPLAYERS + 1]; -new bool:g_bPlayerProxyAvailable[MAXPLAYERS + 1]; -new Handle:g_hPlayerProxyAvailableTimer[MAXPLAYERS + 1]; -new bool:g_bPlayerProxyAvailableInForce[MAXPLAYERS + 1]; -new g_iPlayerProxyAvailableCount[MAXPLAYERS + 1]; -new g_iPlayerProxyMaster[MAXPLAYERS + 1]; -new g_iPlayerProxyControl[MAXPLAYERS + 1]; -new Handle:g_hPlayerProxyControlTimer[MAXPLAYERS + 1]; -new Float:g_flPlayerProxyControlRate[MAXPLAYERS + 1]; -new Handle:g_flPlayerProxyVoiceTimer[MAXPLAYERS + 1]; -new g_iPlayerProxyAskMaster[MAXPLAYERS + 1] = { -1, ... }; -new Float:g_iPlayerProxyAskPosition[MAXPLAYERS + 1][3]; - -new g_iPlayerDesiredFOV[MAXPLAYERS + 1]; - -new Handle:g_hPlayerPostWeaponsTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Music system. -new g_iPlayerMusicFlags[MAXPLAYERS + 1]; -new String:g_strPlayerMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerMusicVolume[MAXPLAYERS + 1]; -new Float:g_flPlayerMusicTargetVolume[MAXPLAYERS + 1]; -new Handle:g_hPlayerMusicTimer[MAXPLAYERS + 1]; -new g_iPlayerPageMusicMaster[MAXPLAYERS + 1]; - -// Chase music system, which apparently also uses the alert song system. And the idle sound system. -new String:g_strPlayerChaseMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new String:g_strPlayerChaseMusicSee[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerChaseMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Float:g_flPlayerChaseMusicSeeVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerChaseMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerChaseMusicSeeTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayerChaseMusicMaster[MAXPLAYERS + 1] = { -1, ... }; -new g_iPlayerChaseMusicSeeMaster[MAXPLAYERS + 1] = { -1, ... }; - -new String:g_strPlayerAlertMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayerAlertMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayerAlertMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayerAlertMusicMaster[MAXPLAYERS + 1] = { -1, ... }; - -new String:g_strPlayer20DollarsMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; -new Float:g_flPlayer20DollarsMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; -new Handle:g_hPlayer20DollarsMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; -new g_iPlayer20DollarsMusicMaster[MAXPLAYERS + 1] = { -1, ... }; - - -new SF2RoundState:g_iRoundState = SF2RoundState_Invalid; -new bool:g_bRoundGrace = false; -new Float:g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; -new bool:g_bRoundInfiniteFlashlight = false; -new bool:g_bRoundInfiniteBlink = false; -new bool:g_bRoundInfiniteSprint = false; - -static Handle:g_hRoundGraceTimer = INVALID_HANDLE; -static Handle:g_hRoundTimer = INVALID_HANDLE; -static Handle:g_hVoteTimer = INVALID_HANDLE; -static String:g_strRoundBossProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - -static g_iRoundCount = 0; -static g_iRoundEndCount = 0; -static g_iRoundActiveCount = 0; -static g_iRoundTime = 0; -static g_iRoundTimeLimit = 0; -static g_iRoundEscapeTimeLimit = 0; -static g_iRoundTimeGainFromPage = 0; -static bool:g_bRoundHasEscapeObjective = false; - -static g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; - -static g_iRoundIntroFadeColor[4] = { 255, ... }; -static Float:g_flRoundIntroFadeHoldTime; -static Float:g_flRoundIntroFadeDuration; -static Handle:g_hRoundIntroTimer = INVALID_HANDLE; -static bool:g_bRoundIntroTextDefault = true; -static Handle:g_hRoundIntroTextTimer = INVALID_HANDLE; -static g_iRoundIntroText; -static String:g_strRoundIntroMusic[PLATFORM_MAX_PATH] = ""; - -static g_iRoundWarmupRoundCount = 0; - -static bool:g_bRoundWaitingForPlayers = false; - -// Special round variables. -new bool:g_bSpecialRound = false; -new g_iSpecialRoundType = 0; -new bool:g_bSpecialRoundNew = false; -new bool:g_bSpecialRoundContinuous = false; -new g_iSpecialRoundCount = 1; -new bool:g_bPlayerPlayedSpecialRound[MAXPLAYERS + 1] = { true, ... }; - -// New boss round variables. -static bool:g_bNewBossRound = false; -static bool:g_bNewBossRoundNew = false; -static bool:g_bNewBossRoundContinuous = false; -static g_iNewBossRoundCount = 1; -static bool:g_bPlayerPlayedNewBossRound[MAXPLAYERS + 1] = { true, ... }; -static String:g_strNewBossRoundProfile[64] = ""; - -static Handle:g_hRoundMessagesTimer = INVALID_HANDLE; -static g_iRoundMessagesNum = 0; - -static Handle:g_hBossCountUpdateTimer = INVALID_HANDLE; -static Handle:g_hClientAverageUpdateTimer = INVALID_HANDLE; - -// Server variables. -new Handle:g_cvVersion; -new Handle:g_cvEnabled; -new Handle:g_cvSlenderMapsOnly; -new Handle:g_cvPlayerViewbobEnabled; -new Handle:g_cvPlayerShakeEnabled; -new Handle:g_cvPlayerShakeFrequencyMax; -new Handle:g_cvPlayerShakeAmplitudeMax; -new Handle:g_cvGraceTime; -new Handle:g_cvAllChat; -new Handle:g_cv20Dollars; -new Handle:g_cvMaxPlayers; -new Handle:g_cvMaxPlayersOverride; -new Handle:g_cvCampingEnabled; -new Handle:g_cvCampingMaxStrikes; -new Handle:g_cvCampingStrikesWarn; -new Handle:g_cvCampingMinDistance; -new Handle:g_cvCampingNoStrikeSanity; -new Handle:g_cvCampingNoStrikeBossDistance; -new Handle:g_cvDifficulty; -new Handle:g_cvBossMain; -new Handle:g_cvBossProfileOverride; -new Handle:g_cvPlayerBlinkRate; -new Handle:g_cvPlayerBlinkHoldTime; -new Handle:g_cvSpecialRoundBehavior; -new Handle:g_cvSpecialRoundForce; -new Handle:g_cvSpecialRoundOverride; -new Handle:g_cvSpecialRoundInterval; -new Handle:g_cvNewBossRoundBehavior; -new Handle:g_cvNewBossRoundInterval; -new Handle:g_cvNewBossRoundForce; -new Handle:g_cvPlayerVoiceDistance; -new Handle:g_cvPlayerVoiceWallScale; -new Handle:g_cvUltravisionEnabled; -new Handle:g_cvUltravisionRadiusRed; -new Handle:g_cvUltravisionRadiusBlue; -new Handle:g_cvUltravisionBrightness; -new Handle:g_cvGhostModeConnectionCheck; -new Handle:g_cvGhostModeConnectionTolerance; -new Handle:g_cvIntroEnabled; -new Handle:g_cvIntroDefaultHoldTime; -new Handle:g_cvIntroDefaultFadeTime; -new Handle:g_cvTimeLimit; -new Handle:g_cvTimeLimitEscape; -new Handle:g_cvTimeGainFromPageGrab; -new Handle:g_cvWarmupRound; -new Handle:g_cvWarmupRoundNum; -new Handle:g_cvPlayerViewbobHurtEnabled; -new Handle:g_cvPlayerViewbobSprintEnabled; -new Handle:g_cvPlayerFakeLagCompensation; -new Handle:g_cvPlayerProxyWaitTime; -new Handle:g_cvPlayerProxyAsk; -new Handle:g_cvHalfZatoichiHealthGain; -new Handle:g_cvBlockSuicideDuringRound; - -new Handle:g_cvPlayerInfiniteSprintOverride; -new Handle:g_cvPlayerInfiniteFlashlightOverride; -new Handle:g_cvPlayerInfiniteBlinkOverride; - -new Handle:g_cvGravity; -new Float:g_flGravity; - -new Handle:g_cvMaxRounds; - -new bool:g_b20Dollars; - -new bool:g_bPlayerShakeEnabled; -new bool:g_bPlayerViewbobEnabled; -new bool:g_bPlayerViewbobHurtEnabled; -new bool:g_bPlayerViewbobSprintEnabled; - -new Handle:g_hHudSync; -new Handle:g_hHudSync2; -new Handle:g_hRoundTimerSync; - -new Handle:g_hCookie; - -// Global forwards. -new Handle:fOnBossAdded; -new Handle:fOnBossSpawn; -new Handle:fOnBossChangeState; -new Handle:fOnBossRemoved; -new Handle:fOnPagesSpawned; -new Handle:fOnClientBlink; -new Handle:fOnClientCaughtByBoss; -new Handle:fOnClientGiveQueuePoints; -new Handle:fOnClientActivateFlashlight; -new Handle:fOnClientDeactivateFlashlight; -new Handle:fOnClientBreakFlashlight; -new Handle:fOnClientEscape; -new Handle:fOnClientLooksAtBoss; -new Handle:fOnClientLooksAwayFromBoss; -new Handle:fOnClientStartDeathCam; -new Handle:fOnClientEndDeathCam; -new Handle:fOnClientGetDefaultWalkSpeed; -new Handle:fOnClientGetDefaultSprintSpeed; -new Handle:fOnClientSpawnedAsProxy; -new Handle:fOnClientDamagedByBoss; -new Handle:fOnGroupGiveQueuePoints; - -new Handle:g_hSDKWeaponScattergun; -new Handle:g_hSDKWeaponPistolScout; -new Handle:g_hSDKWeaponBat; -new Handle:g_hSDKWeaponSniperRifle; -new Handle:g_hSDKWeaponSMG; -new Handle:g_hSDKWeaponKukri; -new Handle:g_hSDKWeaponRocketLauncher; -new Handle:g_hSDKWeaponShotgunSoldier; -new Handle:g_hSDKWeaponShovel; -new Handle:g_hSDKWeaponGrenadeLauncher; -new Handle:g_hSDKWeaponStickyLauncher; -new Handle:g_hSDKWeaponBottle; -new Handle:g_hSDKWeaponMinigun; -new Handle:g_hSDKWeaponShotgunHeavy; -new Handle:g_hSDKWeaponFists; -new Handle:g_hSDKWeaponSyringeGun; -new Handle:g_hSDKWeaponMedigun; -new Handle:g_hSDKWeaponBonesaw; -new Handle:g_hSDKWeaponFlamethrower; -new Handle:g_hSDKWeaponShotgunPyro; -new Handle:g_hSDKWeaponFireaxe; -new Handle:g_hSDKWeaponRevolver; -new Handle:g_hSDKWeaponKnife; -new Handle:g_hSDKWeaponInvis; -new Handle:g_hSDKWeaponShotgunPrimary; -new Handle:g_hSDKWeaponPistol; -new Handle:g_hSDKWeaponWrench; - -new Handle:g_hSDKGetMaxHealth; -new Handle:g_hSDKWantsLagCompensationOnEntity; -new Handle:g_hSDKShouldTransmit; - -#include "rytp_horror/stocks.sp" -#include "rytp_horror/overlay.sp" -#include "rytp_horror/logging.sp" -#include "rytp_horror/debug.sp" -#include "rytp_horror/profiles.sp" -#include "rytp_horror/nav.sp" -#include "rytp_horror/effects.sp" -#include "rytp_horror/playergroups.sp" -#include "rytp_horror/menus.sp" -#include "rytp_horror/pvp.sp" -#include "rytp_horror/client.sp" -#include "rytp_horror/npc.sp" -#include "rytp_horror/specialround.sp" -#include "rytp_horror/adminmenu.sp" - - -#define SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND "ui/item_acquired.wav" - -// ========================================================== -// GENERAL PLUGIN HOOK FUNCTIONS -// ========================================================== - -public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) -{ - RegPluginLibrary("sf2"); - - fOnBossAdded = CreateGlobalForward("SF2_OnBossAdded", ET_Ignore, Param_Cell); - fOnBossSpawn = CreateGlobalForward("SF2_OnBossSpawn", ET_Ignore, Param_Cell); - fOnBossChangeState = CreateGlobalForward("SF2_OnBossChangeState", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); - fOnBossRemoved = CreateGlobalForward("SF2_OnBossRemoved", ET_Ignore, Param_Cell); - fOnPagesSpawned = CreateGlobalForward("SF2_OnPagesSpawned", ET_Ignore); - fOnClientBlink = CreateGlobalForward("SF2_OnClientBlink", ET_Ignore, Param_Cell); - fOnClientCaughtByBoss = CreateGlobalForward("SF2_OnClientCaughtByBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientGiveQueuePoints = CreateGlobalForward("SF2_OnClientGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientActivateFlashlight = CreateGlobalForward("SF2_OnClientActivateFlashlight", ET_Ignore, Param_Cell); - fOnClientDeactivateFlashlight = CreateGlobalForward("SF2_OnClientDeactivateFlashlight", ET_Ignore, Param_Cell); - fOnClientBreakFlashlight = CreateGlobalForward("SF2_OnClientBreakFlashlight", ET_Ignore, Param_Cell); - fOnClientEscape = CreateGlobalForward("SF2_OnClientEscape", ET_Ignore, Param_Cell); - fOnClientLooksAtBoss = CreateGlobalForward("SF2_OnClientLooksAtBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientLooksAwayFromBoss = CreateGlobalForward("SF2_OnClientLooksAwayFromBoss", ET_Ignore, Param_Cell, Param_Cell); - fOnClientStartDeathCam = CreateGlobalForward("SF2_OnClientStartDeathCam", ET_Ignore, Param_Cell, Param_Cell); - fOnClientEndDeathCam = CreateGlobalForward("SF2_OnClientEndDeathCam", ET_Ignore, Param_Cell, Param_Cell); - fOnClientGetDefaultWalkSpeed = CreateGlobalForward("SF2_OnClientGetDefaultWalkSpeed", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientGetDefaultSprintSpeed = CreateGlobalForward("SF2_OnClientGetDefaultSprintSpeed", ET_Hook, Param_Cell, Param_CellByRef); - fOnClientSpawnedAsProxy = CreateGlobalForward("SF2_OnClientSpawnedAsProxy", ET_Ignore, Param_Cell); - fOnClientDamagedByBoss = CreateGlobalForward("SF2_OnClientDamagedByBoss", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell); - fOnGroupGiveQueuePoints = CreateGlobalForward("SF2_OnGroupGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); - - CreateNative("SF2_IsRunning", Native_IsRunning); - CreateNative("SF2_GetCurrentDifficulty", Native_GetCurrentDifficulty); - CreateNative("SF2_GetDifficultyModifier", Native_GetDifficultyModifier); - CreateNative("SF2_IsClientEliminated", Native_IsClientEliminated); - CreateNative("SF2_IsClientInGhostMode", Native_IsClientInGhostMode); - CreateNative("SF2_IsClientProxy", Native_IsClientProxy); - CreateNative("SF2_GetClientBlinkCount", Native_GetClientBlinkCount); - CreateNative("SF2_GetClientProxyMaster", Native_GetClientProxyMaster); - CreateNative("SF2_GetClientProxyControlAmount", Native_GetClientProxyControlAmount); - CreateNative("SF2_GetClientProxyControlRate", Native_GetClientProxyControlRate); - CreateNative("SF2_SetClientProxyMaster", Native_SetClientProxyMaster); - CreateNative("SF2_SetClientProxyControlAmount", Native_SetClientProxyControlAmount); - CreateNative("SF2_SetClientProxyControlRate", Native_SetClientProxyControlRate); - CreateNative("SF2_IsClientLookingAtBoss", Native_IsClientLookingAtBoss); - CreateNative("SF2_CollectAsPage", Native_CollectAsPage); - CreateNative("SF2_GetMaxBossCount", Native_GetMaxBosses); - CreateNative("SF2_EntIndexToBossIndex", Native_EntIndexToBossIndex); - CreateNative("SF2_BossIndexToEntIndex", Native_BossIndexToEntIndex); - CreateNative("SF2_BossIDToBossIndex", Native_BossIDToBossIndex); - CreateNative("SF2_BossIndexToBossID", Native_BossIndexToBossID); - CreateNative("SF2_GetBossName", Native_GetBossName); - CreateNative("SF2_GetBossModelEntity", Native_GetBossModelEntity); - CreateNative("SF2_GetBossTarget", Native_GetBossTarget); - CreateNative("SF2_GetBossMaster", Native_GetBossMaster); - CreateNative("SF2_GetBossState", Native_GetBossState); - CreateNative("SF2_IsBossProfileValid", Native_IsBossProfileValid); - CreateNative("SF2_GetBossProfileNum", Native_GetBossProfileNum); - CreateNative("SF2_GetBossProfileFloat", Native_GetBossProfileFloat); - CreateNative("SF2_GetBossProfileString", Native_GetBossProfileString); - CreateNative("SF2_GetBossProfileVector", Native_GetBossProfileVector); - CreateNative("SF2_GetRandomStringFromBossProfile", Native_GetRandomStringFromBossProfile); - - PvP_InitializeAPI(); - - SpecialRoundInitializeAPI(); - - return APLRes_Success; -} - -public OnPluginStart() -{ - LoadTranslations("core.phrases"); - LoadTranslations("common.phrases"); - LoadTranslations("sf2.phrases"); - - // Get offsets. - g_offsPlayerFOV = FindSendPropInfo("CBasePlayer", "m_iFOV"); - if (g_offsPlayerFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iFOV."); - - g_offsPlayerDefaultFOV = FindSendPropInfo("CBasePlayer", "m_iDefaultFOV"); - if (g_offsPlayerDefaultFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iDefaultFOV."); - - g_offsPlayerFogCtrl = FindSendPropInfo("CBasePlayer", "m_PlayerFog.m_hCtrl"); - if (g_offsPlayerFogCtrl == -1) LogError("Couldn't find CBasePlayer offset for m_PlayerFog.m_hCtrl!"); - - g_offsPlayerPunchAngle = FindSendPropInfo("CBasePlayer", "m_vecPunchAngle"); - if (g_offsPlayerPunchAngle == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngle!"); - - g_offsPlayerPunchAngleVel = FindSendPropInfo("CBasePlayer", "m_vecPunchAngleVel"); - if (g_offsPlayerPunchAngleVel == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngleVel!"); - - g_offsFogCtrlEnable = FindSendPropInfo("CFogController", "m_fog.enable"); - if (g_offsFogCtrlEnable == -1) LogError("Couldn't find CFogController offset for m_fog.enable!"); - - g_offsFogCtrlEnd = FindSendPropInfo("CFogController", "m_fog.end"); - if (g_offsFogCtrlEnd == -1) LogError("Couldn't find CFogController offset for m_fog.end!"); - - g_hPageMusicRanges = CreateArray(3); - - // Register console variables. - g_cvVersion = CreateConVar("sf2_version", PLUGIN_VERSION, "The current version of Slender Fortress. DO NOT TOUCH!", FCVAR_SPONLY | FCVAR_NOTIFY | FCVAR_DONTRECORD); - SetConVarString(g_cvVersion, PLUGIN_VERSION); - - g_cvEnabled = CreateConVar("sf2_enabled", "1", "Enable/Disable the Slender Fortress gamemode. This will take effect on map change.", FCVAR_NOTIFY | FCVAR_DONTRECORD); - g_cvSlenderMapsOnly = CreateConVar("sf2_slendermapsonly", "1", "Only enable the Slender Fortress gamemode on map names prefixed with \"slender_\" or \"sf2_\"."); - - g_cvGraceTime = CreateConVar("sf2_gracetime", "30.0"); - g_cvIntroEnabled = CreateConVar("sf2_intro_enabled", "1"); - g_cvIntroDefaultHoldTime = CreateConVar("sf2_intro_default_hold_time", "9.0"); - g_cvIntroDefaultFadeTime = CreateConVar("sf2_intro_default_fade_time", "1.0"); - - g_cvBlockSuicideDuringRound = CreateConVar("sf2_block_suicide_during_round", "0"); - - g_cvAllChat = CreateConVar("sf2_alltalk", "0"); - HookConVarChange(g_cvAllChat, OnConVarChanged); - - g_cvPlayerVoiceDistance = CreateConVar("sf2_player_voice_distance", "800.0", "The maximum distance RED can communicate in voice chat. Set to 0 if you want them to be heard at all times.", _, true, 0.0); - g_cvPlayerVoiceWallScale = CreateConVar("sf2_player_voice_scale_blocked", "0.5", "The distance required to hear RED in voice chat will be multiplied by this amount if something is blocking them."); - - g_cvPlayerViewbobEnabled = CreateConVar("sf2_player_viewbob_enabled", "1", "Enable/Disable player viewbobbing.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobEnabled, OnConVarChanged); - g_cvPlayerViewbobHurtEnabled = CreateConVar("sf2_player_viewbob_hurt_enabled", "0", "Enable/Disable player view tilting when hurt.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobHurtEnabled, OnConVarChanged); - g_cvPlayerViewbobSprintEnabled = CreateConVar("sf2_player_viewbob_sprint_enabled", "0", "Enable/Disable player step viewbobbing when sprinting.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerViewbobSprintEnabled, OnConVarChanged); - g_cvGravity = FindConVar("sv_gravity"); - HookConVarChange(g_cvGravity, OnConVarChanged); - - g_cvPlayerFakeLagCompensation = CreateConVar("sf2_player_fakelagcompensation", "0", "(EXPERIMENTAL) Enable/Disable fake lag compensation for some hitscan weapons such as the Sniper Rifle.", _, true, 0.0, true, 1.0); - - g_cvPlayerShakeEnabled = CreateConVar("sf2_player_shake_enabled", "1", "Enable/Disable player view shake during boss encounters.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cvPlayerShakeEnabled, OnConVarChanged); - g_cvPlayerShakeFrequencyMax = CreateConVar("sf2_player_shake_frequency_max", "255", "Maximum frequency value of the shake. Should be a value between 1-255.", _, true, 1.0, true, 255.0); - g_cvPlayerShakeAmplitudeMax = CreateConVar("sf2_player_shake_amplitude_max", "5", "Maximum amplitude value of the shake. Should be a value between 1-16.", _, true, 1.0, true, 16.0); - - g_cvPlayerBlinkRate = CreateConVar("sf2_player_blink_rate", "0.33", "How long (in seconds) each bar on the player's Blink meter lasts.", _, true, 0.0); - g_cvPlayerBlinkHoldTime = CreateConVar("sf2_player_blink_holdtime", "0.15", "How long (in seconds) a player will stay in Blink mode when he or she blinks.", _, true, 0.0); - - g_cvUltravisionEnabled = CreateConVar("sf2_player_ultravision_enabled", "1", "Enable/Disable player Ultravision. This helps players see in the dark when their Flashlight is off or unavailable.", _, true, 0.0, true, 1.0); - g_cvUltravisionRadiusRed = CreateConVar("sf2_player_ultravision_radius_red", "512.0"); - g_cvUltravisionRadiusBlue = CreateConVar("sf2_player_ultravision_radius_blue", "800.0"); - g_cvUltravisionBrightness = CreateConVar("sf2_player_ultravision_brightness", "-4"); - - g_cvGhostModeConnectionCheck = CreateConVar("sf2_ghostmode_check_connection", "1", "Checks a player's connection while in Ghost Mode. If the check fails, the client is booted out of Ghost Mode and the action and client's SteamID is logged in the main SF2 log."); - g_cvGhostModeConnectionTolerance = CreateConVar("sf2_ghostmode_connection_tolerance", "5.0", "If sf2_ghostmode_check_connection is set to 1 and the client has timed out for at least this amount of time, the client will be booted out of Ghost Mode."); - - g_cv20Dollars = CreateConVar("sf2_20dollarmode", "0", "Enable/Disable $20 mode.", _, true, 0.0, true, 1.0); - HookConVarChange(g_cv20Dollars, OnConVarChanged); - - g_cvMaxPlayers = CreateConVar("sf2_maxplayers", "5", "The maximum amount of players that can be in one round.", _, true, 1.0); - HookConVarChange(g_cvMaxPlayers, OnConVarChanged); - - g_cvMaxPlayersOverride = CreateConVar("sf2_maxplayers_override", "-1", "Overrides the maximum amount of players that can be in one round.", _, true, -1.0); - HookConVarChange(g_cvMaxPlayersOverride, OnConVarChanged); - - g_cvCampingEnabled = CreateConVar("sf2_anticamping_enabled", "1", "Enable/Disable anti-camping system for RED.", _, true, 0.0, true, 1.0); - g_cvCampingMaxStrikes = CreateConVar("sf2_anticamping_maxstrikes", "4", "How many 5-second intervals players are allowed to stay in one spot before he/she is forced to suicide.", _, true, 0.0); - g_cvCampingStrikesWarn = CreateConVar("sf2_anticamping_strikeswarn", "2", "The amount of strikes left where the player will be warned of camping."); - g_cvCampingMinDistance = CreateConVar("sf2_anticamping_mindistance", "128.0", "Every 5 seconds the player has to be at least this far away from his last position 5 seconds ago or else he'll get a strike."); - g_cvCampingNoStrikeSanity = CreateConVar("sf2_anticamping_no_strike_sanity", "0.1", "The camping system will NOT give any strikes under any circumstances if the players's Sanity is missing at least this much of his maximum Sanity (max is 1.0)."); - g_cvCampingNoStrikeBossDistance = CreateConVar("sf2_anticamping_no_strike_boss_distance", "512.0", "The camping system will NOT give any strikes under any circumstances if the player is this close to a boss (ignoring LOS)."); - g_cvBossMain = CreateConVar("sf2_boss_main", "slenderman", "The name of the main boss (its profile name, not its display name)"); - g_cvBossProfileOverride = CreateConVar("sf2_boss_profile_override", "", "Overrides which boss will be chosen next. Only applies to the first boss being chosen."); - g_cvDifficulty = CreateConVar("sf2_difficulty", "1", "Difficulty of the game. 1 = Normal, 2 = Hard, 3 = Insane.", _, true, 1.0, true, 3.0); - HookConVarChange(g_cvDifficulty, OnConVarChanged); - - g_cvSpecialRoundBehavior = CreateConVar("sf2_specialround_mode", "0", "0 = Special Round resets on next round, 1 = Special Round keeps going until all players have played (not counting spectators, recently joined players, and those who reset their queue points during the round)", _, true, 0.0, true, 1.0); - g_cvSpecialRoundForce = CreateConVar("sf2_specialround_forceenable", "-1", "Sets whether a Special Round will occur on the next round or not.", _, true, -1.0, true, 1.0); - g_cvSpecialRoundOverride = CreateConVar("sf2_specialround_forcetype", "-1", "Sets the type of Special Round that will be chosen on the next Special Round. Set to -1 to let the game choose.", _, true, -1.0); - g_cvSpecialRoundInterval = CreateConVar("sf2_specialround_interval", "5", "If this many rounds are completed, the next round will be a Special Round.", _, true, 0.0); - - g_cvNewBossRoundBehavior = CreateConVar("sf2_newbossround_mode", "0", "0 = boss selection will return to normal after the boss round, 1 = the new boss will continue being the boss until all players in the server have played against it (not counting spectators, recently joined players, and those who reset their queue points during the round).", _, true, 0.0, true, 1.0); - g_cvNewBossRoundInterval = CreateConVar("sf2_newbossround_interval", "3", "If this many rounds are completed, the next round's boss will be randomly chosen, but will not be the main boss.", _, true, 0.0); - g_cvNewBossRoundForce = CreateConVar("sf2_newbossround_forceenable", "-1", "Sets whether a new boss will be chosen on the next round or not. Set to -1 to let the game choose.", _, true, -1.0, true, 1.0); - - g_cvTimeLimit = CreateConVar("sf2_timelimit_default", "300", "The time limit of the round. Maps can change the time limit.", _, true, 0.0); - g_cvTimeLimitEscape = CreateConVar("sf2_timelimit_escape_default", "90", "The time limit to escape. Maps can change the time limit.", _, true, 0.0); - g_cvTimeGainFromPageGrab = CreateConVar("sf2_time_gain_page_grab", "12", "The time gained from grabbing a page. Maps can change the time gain amount."); - - g_cvWarmupRound = CreateConVar("sf2_warmupround", "1", "Enables/disables Warmup Rounds after the \"Waiting for Players\" phase.", _, true, 0.0, true, 1.0); - g_cvWarmupRoundNum = CreateConVar("sf2_warmupround_num", "1", "Sets the amount of Warmup Rounds that occur after the \"Waiting for Players\" phase.", _, true, 0.0); - - g_cvPlayerProxyWaitTime = CreateConVar("sf2_player_proxy_waittime", "35", "How long (in seconds) after a player was chosen to be a Proxy must the system wait before choosing him again."); - g_cvPlayerProxyAsk = CreateConVar("sf2_player_proxy_ask", "0", "Set to 1 if the player can choose before becoming a Proxy, set to 0 to force."); - - g_cvHalfZatoichiHealthGain = CreateConVar("sf2_halfzatoichi_healthgain", "20", "How much health should be gained from killing a player with the Half-Zatoichi? Set to -1 for default behavior."); - - g_cvPlayerInfiniteSprintOverride = CreateConVar("sf2_player_infinite_sprint_override", "-1", "1 = infinite sprint, 0 = never have infinite sprint, -1 = let the game choose.", _, true, -1.0, true, 1.0); - g_cvPlayerInfiniteFlashlightOverride = CreateConVar("sf2_player_infinite_flashlight_override", "-1", "1 = infinite flashlight, 0 = never have infinite flashlight, -1 = let the game choose.", _, true, -1.0, true, 1.0); - g_cvPlayerInfiniteBlinkOverride = CreateConVar("sf2_player_infinite_blink_override", "-1", "1 = infinite blink, 0 = never have infinite blink, -1 = let the game choose.", _, true, -1.0, true, 1.0); - - g_cvMaxRounds = FindConVar("mp_maxrounds"); - - g_hHudSync = CreateHudSynchronizer(); - g_hHudSync2 = CreateHudSynchronizer(); - g_hRoundTimerSync = CreateHudSynchronizer(); - g_hCookie = RegClientCookie("slender_cookie", "", CookieAccess_Private); - - // Register console commands. - RegConsoleCmd("sm_sf2", Command_MainMenu); - RegConsoleCmd("sm_slender", Command_MainMenu); - RegConsoleCmd("sm_horror", Command_MainMenu); - RegConsoleCmd("sm_slnext", Command_Next); - RegConsoleCmd("sm_slgroup", Command_Group); - RegConsoleCmd("sm_slgroupname", Command_GroupName); - RegConsoleCmd("sm_slghost", Command_GhostMode); - RegConsoleCmd("sm_slhelp", Command_Help); - RegConsoleCmd("sm_slsettings", Command_Settings); - RegConsoleCmd("sm_slcredits", Command_Credits); - RegConsoleCmd("sm_flashlight", Command_ToggleFlashlight); - RegConsoleCmd("+sprint", Command_SprintOn); - RegConsoleCmd("-sprint", Command_SprintOff); - - RegAdminCmd("sm_sf2_scare", Command_ClientPerformScare, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_spawn_boss", Command_SpawnSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_add_boss", Command_AddSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_add_boss_fake", Command_AddSlenderFake, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_remove_boss", Command_RemoveSlender, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_getbossindexes", Command_GetBossIndexes, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_setplaystate", Command_ForceState, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_boss_attack_waiters", Command_SlenderAttackWaiters, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_boss_no_teleport", Command_SlenderNoTeleport, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_force_proxy", Command_ForceProxy, ADMFLAG_SLAY); - RegAdminCmd("sm_sf2_force_escape", Command_ForceEscape, ADMFLAG_CHEATS); - - // Hook onto existing console commands. - AddCommandListener(Hook_CommandBuild, "build"); - AddCommandListener(Hook_CommandSuicideAttempt, "kill"); - AddCommandListener(Hook_CommandSuicideAttempt, "explode"); - AddCommandListener(Hook_CommandSuicideAttempt, "joinclass"); - AddCommandListener(Hook_CommandSuicideAttempt, "join_class"); - AddCommandListener(Hook_CommandSuicideAttempt, "jointeam"); - AddCommandListener(Hook_CommandSuicideAttempt, "spectate"); - AddCommandListener(Hook_CommandVoiceMenu, "voicemenu"); - AddCommandListener(Hook_CommandSay, "say"); - - // Hook events. - HookEvent("teamplay_round_start", Event_RoundStart); - HookEvent("teamplay_round_win", Event_RoundEnd); - HookEvent("player_team", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("player_team", Event_PlayerTeam); - HookEvent("player_spawn", Event_PlayerSpawn); - HookEvent("player_hurt", Event_PlayerHurt); - HookEvent("post_inventory_application", Event_PostInventoryApplication); - HookEvent("item_found", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("teamplay_teambalanced_player", Event_DontBroadcastToClients, EventHookMode_Pre); - HookEvent("fish_notice", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("fish_notice__arm", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); - HookEvent("player_death", Event_PlayerDeath); - - // Hook entities. - HookEntityOutput("trigger_multiple", "OnStartTouch", Hook_TriggerOnStartTouch); - HookEntityOutput("trigger_multiple", "OnEndTouch", Hook_TriggerOnEndTouch); - - // Hook usermessages. - HookUserMessage(GetUserMessageId("VoiceSubtitle"), Hook_BlockUserMessage, true); - - // Hook sounds. - AddNormalSoundHook(Hook_NormalSound); - - AddTempEntHook("Fire Bullets", Hook_TEFireBullets); - - InitializeBossProfiles(); - - NPCInitialize(); - - SetupMenus(); - - SetupAdminMenu(); - - SetupClassDefaultWeapons(); - - SetupPlayerGroups(); - - PvP_Initialize(); - - // @TODO: When cvars are finalized, set this to true. - AutoExecConfig(false); - -#if defined DEBUG - InitializeDebug(); -#endif -} - -public OnAllPluginsLoaded() -{ - SetupHooks(); -} - -public OnPluginEnd() -{ - StopPlugin(); -} - -static SetupHooks() -{ - // Check SDKHooks gamedata. - new Handle:hConfig = LoadGameConfigFile("sdkhooks.games"); - if (hConfig == INVALID_HANDLE) SetFailState("Couldn't find SDKHooks gamedata!"); - - StartPrepSDKCall(SDKCall_Entity); - PrepSDKCall_SetFromConf(hConfig, SDKConf_Virtual, "GetMaxHealth"); - PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); - if ((g_hSDKGetMaxHealth = EndPrepSDKCall()) == INVALID_HANDLE) - { - SetFailState("Failed to retrieve GetMaxHealth offset from SDKHooks gamedata!"); - } - - CloseHandle(hConfig); - - // Check our own gamedata. - hConfig = LoadGameConfigFile("sf2"); - if (hConfig == INVALID_HANDLE) SetFailState("Could not find SF2 gamedata!"); - - new iOffset = GameConfGetOffset(hConfig, "CTFPlayer::WantsLagCompensationOnEntity"); - g_hSDKWantsLagCompensationOnEntity = DHookCreate(iOffset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, Hook_ClientWantsLagCompensationOnEntity); - if (g_hSDKWantsLagCompensationOnEntity == INVALID_HANDLE) - { - SetFailState("Failed to create hook CTFPlayer::WantsLagCompensationOnEntity offset from SF2 gamedata!"); - } - - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_CBaseEntity); - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_ObjectPtr); - DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_Unknown); - - iOffset = GameConfGetOffset(hConfig, "CBaseEntity::ShouldTransmit"); - g_hSDKShouldTransmit = DHookCreate(iOffset, HookType_Entity, ReturnType_Int, ThisPointer_CBaseEntity, Hook_EntityShouldTransmit); - if (g_hSDKShouldTransmit == INVALID_HANDLE) - { - SetFailState("Failed to create hook CBaseEntity::ShouldTransmit offset from SF2 gamedata!"); - } - - DHookAddParam(g_hSDKShouldTransmit, HookParamType_ObjectPtr); - - CloseHandle(hConfig); -} - -static SetupClassDefaultWeapons() -{ - // Scout - g_hSDKWeaponScattergun = PrepareItemHandle("tf_weapon_scattergun", 13, 0, 0, ""); - g_hSDKWeaponPistolScout = PrepareItemHandle("tf_weapon_pistol", 23, 0, 0, ""); - g_hSDKWeaponBat = PrepareItemHandle("tf_weapon_bat", 0, 0, 0, ""); - - // Sniper - g_hSDKWeaponSniperRifle = PrepareItemHandle("tf_weapon_sniperrifle", 14, 0, 0, ""); - g_hSDKWeaponSMG = PrepareItemHandle("tf_weapon_smg", 16, 0, 0, ""); - g_hSDKWeaponKukri = PrepareItemHandle("tf_weapon_club", 3, 0, 0, ""); - - // Soldier - g_hSDKWeaponRocketLauncher = PrepareItemHandle("tf_weapon_rocketlauncher", 18, 0, 0, ""); - g_hSDKWeaponShotgunSoldier = PrepareItemHandle("tf_weapon_shotgun", 10, 0, 0, ""); - g_hSDKWeaponShovel = PrepareItemHandle("tf_weapon_shovel", 6, 0, 0, ""); - - // Demoman - g_hSDKWeaponGrenadeLauncher = PrepareItemHandle("tf_weapon_grenadelauncher", 19, 0, 0, ""); - g_hSDKWeaponStickyLauncher = PrepareItemHandle("tf_weapon_pipebomblauncher", 20, 0, 0, ""); - g_hSDKWeaponBottle = PrepareItemHandle("tf_weapon_bottle", 1, 0, 0, ""); - - // Heavy - g_hSDKWeaponMinigun = PrepareItemHandle("tf_weapon_minigun", 15, 0, 0, ""); - g_hSDKWeaponShotgunHeavy = PrepareItemHandle("tf_weapon_shotgun", 11, 0, 0, ""); - g_hSDKWeaponFists = PrepareItemHandle("tf_weapon_fists", 5, 0, 0, ""); - - // Medic - g_hSDKWeaponSyringeGun = PrepareItemHandle("tf_weapon_syringegun_medic", 17, 0, 0, ""); - g_hSDKWeaponMedigun = PrepareItemHandle("tf_weapon_medigun", 29, 0, 0, ""); - g_hSDKWeaponBonesaw = PrepareItemHandle("tf_weapon_bonesaw", 8, 0, 0, ""); - - // Pyro - g_hSDKWeaponFlamethrower = PrepareItemHandle("tf_weapon_flamethrower", 21, 0, 0, "254 ; 4.0"); - g_hSDKWeaponShotgunPyro = PrepareItemHandle("tf_weapon_shotgun", 12, 0, 0, ""); - g_hSDKWeaponFireaxe = PrepareItemHandle("tf_weapon_fireaxe", 2, 0, 0, ""); - - // Spy - g_hSDKWeaponRevolver = PrepareItemHandle("tf_weapon_revolver", 24, 0, 0, ""); - g_hSDKWeaponKnife = PrepareItemHandle("tf_weapon_knife", 4, 0, 0, ""); - g_hSDKWeaponInvis = PrepareItemHandle("tf_weapon_invis", 297, 0, 0, ""); - - // Engineer - g_hSDKWeaponShotgunPrimary = PrepareItemHandle("tf_weapon_shotgun", 9, 0, 0, ""); - g_hSDKWeaponPistol = PrepareItemHandle("tf_weapon_pistol", 22, 0, 0, ""); - g_hSDKWeaponWrench = PrepareItemHandle("tf_weapon_wrench", 7, 0, 0, ""); -} - -public OnMapStart() -{ - PvP_OnMapStart(); -} - -public OnConfigsExecuted() -{ - if (!GetConVarBool(g_cvEnabled)) - { - StopPlugin(); - } - else - { - if (GetConVarBool(g_cvSlenderMapsOnly)) - { - decl String:sMap[256]; - GetCurrentMap(sMap, sizeof(sMap)); - - if (!StrContains(sMap, "slender_", false) || !StrContains(sMap, "sf2_", false)) - { - StartPlugin(); - } - else - { - LogMessage("%s is not a Slender Fortress map. Plugin disabled!", sMap); - StopPlugin(); - } - } - else - { - StartPlugin(); - } - } -} - -static StartPlugin() -{ - if (g_bEnabled) return; - - g_bEnabled = true; - - InitializeLogging(); - -#if defined DEBUG - InitializeDebugLogging(); -#endif - - // Handle ConVars. - new Handle:hCvar = FindConVar("mp_friendlyfire"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mp_flashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mat_supportflashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); - - hCvar = FindConVar("mp_autoteambalance"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - g_flGravity = GetConVarFloat(g_cvGravity); - - g_b20Dollars = GetConVarBool(g_cv20Dollars); - - g_bPlayerShakeEnabled = GetConVarBool(g_cvPlayerShakeEnabled); - g_bPlayerViewbobEnabled = GetConVarBool(g_cvPlayerViewbobEnabled); - g_bPlayerViewbobHurtEnabled = GetConVarBool(g_cvPlayerViewbobHurtEnabled); - g_bPlayerViewbobSprintEnabled = GetConVarBool(g_cvPlayerViewbobSprintEnabled); - - decl String:sBuffer[64]; - Format(sBuffer, sizeof(sBuffer), "RYTP Horror", PLUGIN_VERSION_DISPLAY); - Steam_SetGameDescription(sBuffer); - - PrecacheStuff(); - - // Reset special round. - g_bSpecialRound = false; - g_bSpecialRoundNew = false; - g_bSpecialRoundContinuous = false; - g_iSpecialRoundCount = 1; - g_iSpecialRoundType = 0; - - SpecialRoundReset(); - - // Reset boss rounds. - g_bNewBossRound = false; - g_bNewBossRoundNew = false; - g_bNewBossRoundContinuous = false; - g_iNewBossRoundCount = 1; - strcopy(g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile), ""); - - // Reset global round vars. - g_iRoundCount = 0; - g_iRoundEndCount = 0; - g_iRoundActiveCount = 0; - g_iRoundState = SF2RoundState_Invalid; - g_hRoundMessagesTimer = CreateTimer(200.0, Timer_RoundMessages, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_iRoundMessagesNum = 0; - - g_iRoundWarmupRoundCount = 0; - - g_hClientAverageUpdateTimer = CreateTimer(0.2, Timer_ClientAverageUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_hBossCountUpdateTimer = CreateTimer(2.0, Timer_BossCountUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - SetRoundState(SF2RoundState_Waiting); - - ReloadBossProfiles(); - ReloadRestrictedWeapons(); - ReloadSpecialRounds(); - - NPCOnConfigsExecuted(); - - InitializeBossPackVotes(); - SetupTimeLimitTimerForBossPackVote(); - - // Late load compensation. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - OnClientPutInServer(i); - } -} - -static PrecacheStuff() -{ - // Initialize particles. - g_iParticleCriticalHit = PrecacheParticleSystem(CRIT_PARTICLENAME); - - PrecacheSound2(CRIT_SOUND); - - // simple_bot; - PrecacheModel("models/humans/group01/female_01.mdl", true); - - PrecacheModel(PAGE_MODEL, true); - PrecacheModel(GHOST_MODEL, true); - - PrecacheSound2(FLASHLIGHT_CLICKSOUND); - PrecacheSound2(FLASHLIGHT_BREAKSOUND); - PrecacheSound2(FLASHLIGHT_NOSOUND); - PrecacheSound2(PAGE_GRABSOUND); - - PrecacheSound2(MUSIC_GOTPAGES1_SOUND); - PrecacheSound2(MUSIC_GOTPAGES2_SOUND); - PrecacheSound2(MUSIC_GOTPAGES3_SOUND); - PrecacheSound2(MUSIC_GOTPAGES4_SOUND); - - PrecacheSound2(SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); - - for (new i = 0; i < sizeof(g_strPlayerBreathSounds); i++) - { - PrecacheSound2(g_strPlayerBreathSounds[i]); - } - - // Special round. - PrecacheSound2(SR_MUSIC); - PrecacheSound2(SR_SOUND_SELECT); - PrecacheSound2(SF2_INTRO_DEFAULT_MUSIC); - - PrecacheMaterial2(SF2_OVERLAY_DEFAULT); - PrecacheMaterial2(SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); - PrecacheMaterial2(SF2_OVERLAY_GHOST); - - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.mdl"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx80.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx90.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.phy"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.sw.vtx"); - AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.vvd"); - - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vmt"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vtf"); - AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vmt"); - - // pvp - PvP_Precache(); -} - -static StopPlugin() -{ - if (!g_bEnabled) return; - - g_bEnabled = false; - - // Reset CVars. - new Handle:hCvar = FindConVar("mp_friendlyfire"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - hCvar = FindConVar("mp_flashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - hCvar = FindConVar("mat_supportflashlight"); - if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); - - // Cleanup bosses. - NPCRemoveAll(); - - // Cleanup clients. - for (new i = 1; i <= MaxClients; i++) - { - ClientResetFlashlight(i); - ClientDeactivateUltravision(i); - ClientDisableConstantGlow(i); - ClientRemoveInteractiveGlow(i); - } - - BossProfilesOnMapEnd(); -} - -public OnMapEnd() -{ - StopPlugin(); -} - -public OnMapTimeLeftChanged() -{ - if (g_bEnabled) - { - SetupTimeLimitTimerForBossPackVote(); - } -} - -public TF2_OnConditionAdded(client, TFCond:cond) -{ - if (cond == TFCond_Taunting) - { - if (IsClientInGhostMode(client)) - { - // Stop ghosties from taunting. - TF2_RemoveCondition(client, TFCond_Taunting); - } - } -} - -public OnGameFrame() -{ - if (!g_bEnabled) return; - - // Process through boss movement. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new iBoss = NPCGetEntIndex(i); - if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; - - if (NPCGetFlags(i) & SFF_MARKEDASFAKE) continue; - - new iType = NPCGetType(i); - - switch (iType) - { - case SF2BossType_Static: - { - decl Float:myPos[3], Float:hisPos[3]; - SlenderGetAbsOrigin(i, myPos); - AddVectors(myPos, g_flSlenderEyePosOffset[i], myPos); - - new iBestPlayer = -1; - new Float:flBestDistance = 16384.0; - new Float:flTempDistance; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !IsPlayerAlive(iClient) || IsClientInGhostMode(iClient) || IsClientInDeathCam(iClient)) continue; - if (!IsPointVisibleToPlayer(iClient, myPos, false, false)) continue; - - GetClientAbsOrigin(iClient, hisPos); - - flTempDistance = GetVectorDistance(myPos, hisPos); - if (flTempDistance < flBestDistance) - { - iBestPlayer = iClient; - flBestDistance = flTempDistance; - } - } - - if (iBestPlayer > 0) - { - SlenderGetAbsOrigin(i, myPos); - GetClientAbsOrigin(iBestPlayer, hisPos); - - if (!SlenderOnlyLooksIfNotSeen(i) || !IsPointVisibleToAPlayer(myPos, false, SlenderUsesBlink(i))) - { - new Float:flTurnRate = NPCGetTurnRate(i); - - if (flTurnRate > 0.0) - { - decl Float:flMyEyeAng[3], Float:ang[3]; - GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); - AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[i], flMyEyeAng); - SubtractVectors(hisPos, myPos, ang); - GetVectorAngles(ang, ang); - ang[0] = 0.0; - ang[1] += (AngleDiff(ang[1], flMyEyeAng[1]) >= 0.0 ? 1.0 : -1.0) * flTurnRate * GetTickInterval(); - ang[2] = 0.0; - - // Take care of angle offsets. - AddVectors(ang, g_flSlenderEyePosOffset[i], ang); - for (new i2 = 0; i2 < 3; i2++) ang[i2] = AngleNormalize(ang[i2]); - - TeleportEntity(iBoss, NULL_VECTOR, ang, NULL_VECTOR); - } - } - } - } - case SF2BossType_Chaser: - { - SlenderChaseBossProcessMovement(i); - } - } - } - - PvP_OnGameFrame(); -} - -// ========================================================== -// COMMANDS AND COMMAND HOOK FUNCTIONS -// ========================================================== - -public Action:Command_Help(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuHelp, client, 30); - return Plugin_Handled; -} - -public Action:Command_Settings(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuSettings, client, 30); - return Plugin_Handled; -} - -public Action:Command_Credits(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuCredits, client, MENU_TIME_FOREVER); - return Plugin_Handled; -} - -public Action:Command_ToggleFlashlight(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return Plugin_Handled; - - if (!IsRoundInWarmup() && !IsRoundInIntro() && !IsRoundEnding() && !DidClientEscape(client)) - { - if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) - { - ClientHandleFlashlight(client); - } - } - - return Plugin_Handled; -} - -public Action:Command_SprintOn(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) - { - ClientHandleSprint(client, true); - } - - return Plugin_Handled; -} - -public Action:Command_SprintOff(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) - { - ClientHandleSprint(client, false); - } - - return Plugin_Handled; -} - -public Action:Command_MainMenu(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuMain, client, 30); - return Plugin_Handled; -} - -public Action:Command_Next(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayQueuePointsMenu(client); - return Plugin_Handled; -} - -public Action:Command_Group(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayGroupMainMenuToClient(client); - return Plugin_Handled; -} - -public Action:Command_GroupName(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_slgroupname <name>"); - return Plugin_Handled; - } - - new iGroupIndex = ClientGetPlayerGroup(client); - if (!IsPlayerGroupActive(iGroupIndex)) - { - CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); - return Plugin_Handled; - } - - if (GetPlayerGroupLeader(iGroupIndex) != client) - { - CPrintToChat(client, "%T", "SF2 Not Group Leader", client); - return Plugin_Handled; - } - - decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetCmdArg(1, sGroupName, sizeof(sGroupName)); - if (!sGroupName[0]) - { - CPrintToChat(client, "%T", "SF2 Invalid Group Name", client); - return Plugin_Handled; - } - - decl String:sOldGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; - GetPlayerGroupName(iGroupIndex, sOldGroupName, sizeof(sOldGroupName)); - SetPlayerGroupName(iGroupIndex, sGroupName); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i)) continue; - if (ClientGetPlayerGroup(i) != iGroupIndex) continue; - CPrintToChat(i, "%T", "SF2 Group Name Set", i, sOldGroupName, sGroupName); - } - - return Plugin_Handled; -} - -public Action:Command_GhostMode(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - DisplayMenu(g_hMenuGhostMode, client, 15); - return Plugin_Handled; -} - -public Action:Hook_CommandSay(client, const String:command[], argc) -{ - if (!g_bEnabled || GetConVarBool(g_cvAllChat)) return Plugin_Continue; - - if (!IsRoundEnding()) - { - if (g_bPlayerEliminated[client]) - { - decl String:sMessage[256]; - GetCmdArgString(sMessage, sizeof(sMessage)); - FakeClientCommand(client, "say_team %s", sMessage); - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Hook_CommandSuicideAttempt(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) return Plugin_Handled; - - if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; - - if (GetConVarBool(g_cvBlockSuicideDuringRound)) - { - if (!g_bRoundGrace && !g_bPlayerEliminated[client] && !DidClientEscape(client)) - { - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Hook_CommandBlockInGhostMode(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) return Plugin_Handled; - if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Hook_CommandVoiceMenu(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsClientInGhostMode(client)) - { - ClientGhostModeNextTarget(client); - return Plugin_Handled; - } - - if (g_bPlayerProxy[client]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) - { - return Plugin_Handled; - } - } - } - - return Plugin_Continue; -} - -public Action:Command_ClientPerformScare(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_scare <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32], String:arg2[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - GetCmdArg(2, arg2, sizeof(arg2)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - COMMAND_FILTER_ALIVE, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - ClientPerformScare(target, StringToInt(arg2)); - } - - return Plugin_Handled; -} - -public Action:Command_SpawnSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args == 0) - { - ReplyToCommand(client, "Usage: sm_sf2_spawn_boss <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl Float:eyePos[3], Float:eyeAng[3], Float:endPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(endPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, endPos); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Spawned Boss", client); - LogAction(client, -1, "%N spawned boss %d! (%s)", client, iBossIndex, sProfile); - - return Plugin_Handled; -} - -public Action:Command_RemoveSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args == 0) - { - ReplyToCommand(client, "Usage: sm_sf2_remove_boss <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - NPCRemove(iBossIndex); - - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Removed Boss", client); - LogAction(client, -1, "%N removed boss %d! (%s)", client, iBossIndex, sProfile); - - return Plugin_Handled; -} - -public Action:Command_GetBossIndexes(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - decl String:sMessage[512]; - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - ClientCommand(client, "echo Active Boss Indexes:"); - ClientCommand(client, "echo ----------------------------"); - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - Format(sMessage, sizeof(sMessage), "%d - %s", i, sProfile); - if (NPCGetFlags(i) & SFF_FAKE) - { - StrCat(sMessage, sizeof(sMessage), " (fake)"); - } - - if (g_iSlenderCopyMaster[i] != -1) - { - decl String:sCat[64]; - Format(sCat, sizeof(sCat), " (copy of %d)", g_iSlenderCopyMaster[i]); - StrCat(sMessage, sizeof(sMessage), sCat); - } - - ClientCommand(client, "echo %s", sMessage); - } - - ClientCommand(client, "echo ----------------------------"); - - ReplyToCommand(client, "Printed active boss indexes to your console!"); - - return Plugin_Handled; -} - -public Action:Command_SlenderAttackWaiters(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_boss_attack_waiters <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossFlags = NPCGetFlags(iBossIndex); - - new bool:bState = bool:StringToInt(arg2); - new bool:bOldState = bool:(iBossFlags & SFF_ATTACKWAITERS); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (bState) - { - if (!bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags | SFF_ATTACKWAITERS); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Attack Waiters", client); - LogAction(client, -1, "%N forced boss %d to attack waiters! (%s)", client, iBossIndex, sProfile); - } - } - else - { - if (bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags & ~SFF_ATTACKWAITERS); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Do Not Attack Waiters", client); - LogAction(client, -1, "%N forced boss %d to not attack waiters! (%s)", client, iBossIndex, sProfile); - } - } - - return Plugin_Handled; -} - -public Action:Command_SlenderNoTeleport(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_boss_no_teleport <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - new iBossIndex = StringToInt(arg1); - if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossFlags = NPCGetFlags(iBossIndex); - - new bool:bState = bool:StringToInt(arg2); - new bool:bOldState = bool:(iBossFlags & SFF_NOTELEPORT); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (bState) - { - if (!bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags | SFF_NOTELEPORT); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Not Teleport", client); - LogAction(client, -1, "%N disabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); - } - } - else - { - if (bOldState) - { - NPCSetFlags(iBossIndex, iBossFlags & ~SFF_NOTELEPORT); - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Teleport", client); - LogAction(client, -1, "%N enabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); - } - } - - return Plugin_Handled; -} - -public Action:Command_ForceProxy(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_force_proxy <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); - return Plugin_Handled; - } - - if (IsRoundEnding() || IsRoundInWarmup()) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - 0, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iBossIndex = StringToInt(arg2); - if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) - { - ReplyToCommand(client, "Boss index is out of range!"); - return Plugin_Handled; - } - else if (NPCGetUniqueID(iBossIndex) == -1) - { - ReplyToCommand(client, "Boss index is invalid! Boss index not active!"); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new iTarget = target_list[i]; - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(iTarget, sName, sizeof(sName)); - - if (!g_bPlayerEliminated[iTarget]) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Unable To Perform Action On Player In Round", client, sName); - continue; - } - - if (g_bPlayerProxy[iTarget]) continue; - - decl Float:flNewPos[3]; - - if (!SlenderCalculateNewPlace(iBossIndex, flNewPos, true, true, client)) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Player No Place For Proxy", client, sName); - continue; - } - - ClientEnableProxy(iTarget, iBossIndex); - TeleportEntity(iTarget, flNewPos, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - - LogAction(client, iTarget, "%N forced %N to be a Proxy!", client, iTarget); - } - - return Plugin_Handled; -} - -public Action:Command_ForceEscape(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_force_escape <name|#userid>"); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - COMMAND_FILTER_ALIVE, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - if (!g_bPlayerEliminated[i] && !DidClientEscape(i)) - { - ClientEscape(target); - TeleportClientToEscapePoint(target); - - LogAction(client, target, "%N forced %N to escape!", client, target); - } - } - - return Plugin_Handled; -} - -public Action:Command_AddSlender(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_add_boss <name>"); - return Plugin_Handled; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetCmdArg(1, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - if (!KvJumpToKey(g_hConfig, sProfile)) - { - ReplyToCommand(client, "That boss does not exist!"); - return Plugin_Handled; - } - - new iBossIndex = AddProfile(sProfile); - if (iBossIndex != -1) - { - decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(flPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, flPos); - - LogAction(client, -1, "%N added a boss! (%s)", client, sProfile); - } - - return Plugin_Handled; -} - -public Action:Command_AddSlenderFake(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 1) - { - ReplyToCommand(client, "Usage: sm_sf2_add_boss_fake <name>"); - return Plugin_Handled; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetCmdArg(1, sProfile, sizeof(sProfile)); - - KvRewind(g_hConfig); - if (!KvJumpToKey(g_hConfig, sProfile)) - { - ReplyToCommand(client, "That boss does not exist!"); - return Plugin_Handled; - } - - new iBossIndex = AddProfile(sProfile, SFF_FAKE); - if (iBossIndex != -1) - { - decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); - TR_GetEndPosition(flPos, hTrace); - CloseHandle(hTrace); - - SpawnSlender(iBossIndex, flPos); - - LogAction(client, -1, "%N added a fake boss! (%s)", client, sProfile); - } - - return Plugin_Handled; -} - -public Action:Command_ForceState(client, args) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (args < 2) - { - ReplyToCommand(client, "Usage: sm_sf2_setplaystate <name|#userid> <0/1>"); - return Plugin_Handled; - } - - if (IsRoundEnding() || IsRoundInWarmup()) - { - CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); - return Plugin_Handled; - } - - decl String:arg1[32]; - GetCmdArg(1, arg1, sizeof(arg1)); - - decl String:target_name[MAX_TARGET_LENGTH]; - decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; - - if ((target_count = ProcessTargetString( - arg1, - client, - target_list, - MAXPLAYERS, - 0, - target_name, - sizeof(target_name), - tn_is_ml)) <= 0) - { - ReplyToTargetError(client, target_count); - return Plugin_Handled; - } - - decl String:arg2[32]; - GetCmdArg(2, arg2, sizeof(arg2)); - - new iState = StringToInt(arg2); - - decl String:sName[MAX_NAME_LENGTH]; - - for (new i = 0; i < target_count; i++) - { - new target = target_list[i]; - GetClientName(target, sName, sizeof(sName)); - - if (iState && g_bPlayerEliminated[target]) - { - SetClientPlayState(target, true); - - CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced In Game", sName); - LogAction(client, target, "%N forced %N into the game.", client, target); - } - else if (!iState && !g_bPlayerEliminated[target]) - { - SetClientPlayState(target, false); - - CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced Out Of Game", sName); - LogAction(client, target, "%N took %N out of the game.", client, target); - } - } - - return Plugin_Handled; -} - -public Action:Hook_CommandBuild(client, const String:command[], argc) -{ - if (!g_bEnabled) return Plugin_Continue; - if (!IsClientInPvP(client)) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Timer_BossCountUpdate(Handle:timer) -{ - if (timer != g_hBossCountUpdateTimer) return Plugin_Stop; - - if (!g_bEnabled) return Plugin_Stop; - - new iBossCount = NPCGetCount(); - new iBossPreferredCount; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1 || - g_iSlenderCopyMaster[i] != -1 || - (NPCGetFlags(i) & SFF_FAKE)) - { - continue; - } - - iBossPreferredCount++; - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsValidClient(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - IsClientInGhostMode(i) || - IsClientInDeathCam(i) || - DidClientEscape(i)) continue; - - // Check if we're near any bosses. - new iClosest = -1; - new Float:flBestDist = SF2_BOSS_PAGE_CALCULATION; - - for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) - { - if (NPCGetUniqueID(iBoss) == -1) continue; - if (NPCGetEntIndex(iBoss) == INVALID_ENT_REFERENCE) continue; - if (NPCGetFlags(iBoss) & SFF_FAKE) continue; - - new Float:flDist = NPCGetDistanceFromEntity(iBoss, i); - if (flDist < flBestDist) - { - iClosest = iBoss; - flBestDist = flDist; - break; - } - } - - if (iClosest != -1) continue; - - iClosest = -1; - flBestDist = SF2_BOSS_PAGE_CALCULATION; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient) || - !IsPlayerAlive(iClient) || - g_bPlayerEliminated[iClient] || - IsClientInGhostMode(iClient) || - IsClientInDeathCam(iClient) || - DidClientEscape(iClient)) - { - continue; - } - - new bool:bwub = false; - for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) - { - if (NPCGetUniqueID(iBoss) == -1) continue; - if (NPCGetFlags(iBoss) & SFF_FAKE) continue; - - if (g_iSlenderTarget[iBoss] == iClient) - { - bwub = true; - break; - } - } - - if (!bwub) continue; - - new Float:flDist = EntityDistanceFromEntity(i, iClient); - if (flDist < flBestDist) - { - iClosest = iClient; - flBestDist = flDist; - } - } - - if (!IsValidClient(iClosest)) - { - // No one's close to this dude? DUDE! WE NEED ANOTHER BOSS! - iBossPreferredCount++; - } - } - - new iDiff = iBossCount - iBossPreferredCount; - if (iDiff) - { - if (iDiff > 0) - { - new iCount = iDiff; - // We need less bosses. Try and see if we can remove some. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_iSlenderCopyMaster[i] == -1) continue; - if (PeopleCanSeeSlender(i, _, false)) continue; - if (NPCGetFlags(i) & SFF_FAKE) continue; - - if (SlenderCanRemove(i)) - { - NPCRemove(i); - iCount--; - } - - if (iCount <= 0) - { - break; - } - } - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - new iCount = RoundToFloor(FloatAbs(float(iDiff))); - // Add new bosses (copy of the first boss). - for (new i = 0; i < MAX_BOSSES && iCount > 0; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - if (g_iSlenderCopyMaster[i] != -1) continue; - if (!(NPCGetFlags(i) & SFF_COPIES)) continue; - - // Get the number of copies I already have and see if I can have more copies. - new iCopyCount; - for (new i2 = 0; i2 < MAX_BOSSES; i2++) - { - if (NPCGetUniqueID(i2) == -1) continue; - if (g_iSlenderCopyMaster[i2] != i) continue; - - iCopyCount++; - } - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - if (iCopyCount >= GetProfileNum(sProfile, "copy_max", 10)) - { - continue; - } - - new iBossIndex = AddProfile(sProfile, _, i); - if (iBossIndex == -1) - { - LogError("Could not add copy for %d: No free slots!", i); - } - - iCount--; - } - } - } - - // Check if we can add some proxies. - if (!g_bRoundGrace) - { - if (NavMesh_Exists()) - { - new Handle:hProxyCandidates = CreateArray(); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) continue; - - if (g_iSlenderCopyMaster[iBossIndex] != -1) continue; // Copies cannot generate proxies. - - if (GetGameTime() < g_flSlenderTimeUntilNextProxy[iBossIndex]) continue; // Proxy spawning hasn't cooled down yet. - - new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); - if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) continue; // No teleport target. - - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumActiveProxies = 0; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) == iBossIndex) - { - iNumActiveProxies++; - } - } - - if (iNumActiveProxies >= iMaxProxies) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d has too many active proxies!", iBossIndex); -#endif - continue; - } - - new Float:flSpawnChanceMin = GetProfileFloat(sProfile, "proxies_spawn_chance_min"); - new Float:flSpawnChanceMax = GetProfileFloat(sProfile, "proxies_spawn_chance_max"); - new Float:flSpawnChanceThreshold = GetProfileFloat(sProfile, "proxies_spawn_chance_threshold") * NPCGetAnger(iBossIndex); - - new Float:flChance = GetRandomFloat(flSpawnChanceMin, flSpawnChanceMax); - if (flChance > flSpawnChanceThreshold) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's chances weren't in his favor!", iBossIndex); -#endif - continue; - } - - new iAvailableProxies = iMaxProxies - iNumActiveProxies; - - new iSpawnNumMin = GetProfileNum(sProfile, "proxies_spawn_num_min"); - new iSpawnNumMax = GetProfileNum(sProfile, "proxies_spawn_num_max"); - - new iSpawnNum = 0; - - // Get a list of people we can transform into a good Proxy. - ClearArray(hProxyCandidates); - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (g_bPlayerProxy[iClient]) continue; - - if (!g_iPlayerPreferences[iClient][PlayerPreference_EnableProxySelection]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your preferences.", iBossIndex); -#endif - continue; - } - - if (!g_bPlayerProxyAvailable[iClient]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your cooldown.", iBossIndex); -#endif - continue; - } - - if (g_bPlayerProxyAvailableInForce[iClient]) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're already being forced into a Proxy.", iBossIndex); -#endif - continue; - } - - if (!IsClientParticipating(iClient)) - { -#if defined DEBUG - SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're not participating.", iBossIndex); -#endif - continue; - } - - PushArrayCell(hProxyCandidates, iClient); - iSpawnNum++; - } - - if (iSpawnNum >= iSpawnNumMax) - { - iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNumMax); - } - else if (iSpawnNum >= iSpawnNumMin) - { - iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNum); - } - - if (iSpawnNum <= 0) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d had a set spawn number of 0!", iBossIndex); -#endif - continue; - } - - decl Float:flTargetPos[3]; - GetClientAbsOrigin(iTeleportTarget, flTargetPos); - - new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); - if (iTargetAreaIndex == -1) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's teleport target is not on the navmesh!", iBossIndex); -#endif - continue; // target is not on the nav mesh. - } - - // Search outwards until travel distance is at maximum range. - new Handle:hAreaArray = CreateArray(2); - new Handle:hAreas = CreateStack(); - NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderProxyTeleportMaxRange[iBossIndex]); - - new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderProxyTeleportMinRange[iBossIndex], g_flSlenderProxyTeleportMaxRange[iBossIndex]); - - { - new iAreaIndex = -1; - new iPoppedAreas = 0; - - while (!IsStackEmpty(hAreas)) - { - PopStackCell(hAreas, iAreaIndex); - new iCostSoFar = NavMeshArea_GetCostSoFar(iAreaIndex); - - if (float(iCostSoFar) >= flTeleportMinRange) - { - new iIndex = PushArrayCell(hAreaArray, iAreaIndex); - SetArrayCell(hAreaArray, iIndex, float(iCostSoFar), 1); - iPoppedAreas++; - } - } - - CloseHandle(hAreas); - - if (iPoppedAreas == 0) - { - // no areas to use! - CloseHandle(hAreaArray); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any sufficient surrounding areas!", iBossIndex); -#endif - - continue; - } -#if defined DEBUG - else - { - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d found %d surrounding areas", iBossIndex, iPoppedAreas); - } -#endif - } - - new Handle:hAreaArrayClose = CreateArray(); - new Handle:hAreaArrayAverage = CreateArray(); - new Handle:hAreaArrayFar = CreateArray(); - - for (new iRangeSection = 1; iRangeSection <= 3; iRangeSection++) - { - new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection - 1) / 3.0); - new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection) / 3.0); - - for (new i = 0, iSize = GetArraySize(hAreaArray); i < iSize; i++) - { - new iAreaIndex = GetArrayCell(hAreaArray, i); - - decl Float:flAreaCenter[3]; - NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); - - decl Float:flTestPos[3]; - decl Float:flEyeOffset[3]; - flEyeOffset[0] = 0.0; - flEyeOffset[1] = 0.0; - flEyeOffset[2] = HalfHumanHeight * 2.0; - - // Check visibility first. - if (IsPointVisibleToAPlayer(flAreaCenter, false, false)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (1)", iBossIndex, iAreaIndex); -#endif - continue; - } - - AddVectors(flAreaCenter, flEyeOffset, flTestPos); - - if (IsPointVisibleToAPlayer(flTestPos, false, false)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (2)", iBossIndex, iAreaIndex); -#endif - - continue; - } - - new iBoss = NPCGetEntIndex(iBossIndex); - - // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. - { - decl Float:flTraceStartPos[3]; - flTraceStartPos[0] = flAreaCenter[0]; - flTraceStartPos[1] = flAreaCenter[1]; - flTraceStartPos[2] = flAreaCenter[2] + (HalfHumanHeight * 2.0); - - decl Float:flTraceMins[3]; - flTraceMins[0] = -20.0; - flTraceMins[1] = -20.0; - flTraceMins[2] = 0.0; - - decl Float:flTraceMaxs[3]; - flTraceMaxs[0] = 20.0; - flTraceMaxs[1] = 20.0; - flTraceMaxs[2] = 0.0; - - new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, - flAreaCenter, - flTraceMins, - flTraceMaxs, - MASK_NPCSOLID, - TraceRayDontHitEntity, - iBoss); - - decl Float:flTraceHitPos[3]; - TR_GetEndPosition(flTraceHitPos, hTrace); - flTraceHitPos[2] += 1.0; - CloseHandle(hTrace); - - static Float:flTraceSpaceMin[3] = { -20.0, -20.0, 0.0 }; - static Float:flTraceSpaceMax[3] = { 20.0, 20.0, 72.0 }; - - flTraceSpaceMax[2] = HalfHumanHeight * 2.0; - - if (IsSpaceOccupiedPlayer(flTraceHitPos, - flTraceSpaceMin, - flTraceSpaceMax, - iBoss == INVALID_ENT_REFERENCE ? -1 : iBoss)) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected too small area index %d! (2)", iBossIndex, iAreaIndex); -#endif - - continue; - } - } - - new bool:bTooNear = false; - - // Check minimum range. - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || - !IsPlayerAlive(iClient) || - g_bPlayerEliminated[iClient] || - DidClientEscape(iClient) || - g_bPlayerProxy[iClient] || - IsClientInGhostMode(iClient)) - { - continue; - } - - decl Float:flTempPos[3]; - GetClientAbsOrigin(iClient, flTempPos); - - if (GetVectorDistance(flAreaCenter, flTempPos) <= flTeleportMinRange) - { - bTooNear = true; - break; - } - } - - if (bTooNear) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected near area index %d!", iBossIndex, iAreaIndex); -#endif - - continue; // This area is too close to a player. - } - - // Check travel distance. - new Float:flDist = Float:GetArrayCell(hAreaArray, i, 1); - if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) - { - switch (iRangeSection) - { - case 1: PushArrayCell(hAreaArrayClose, iAreaIndex); - case 2: PushArrayCell(hAreaArrayAverage, iAreaIndex); - case 3: PushArrayCell(hAreaArrayFar, iAreaIndex); - } - } - } - } - - CloseHandle(hAreaArray); - - // Set the cooldown time! - new Float:flSpawnCooldownMin = GetProfileFloat(sProfile, "proxies_spawn_cooldown_min"); - new Float:flSpawnCooldownMax = GetProfileFloat(sProfile, "proxies_spawn_cooldown_max"); - - g_flSlenderTimeUntilNextProxy[iBossIndex] = GetGameTime() + GetRandomFloat(flSpawnCooldownMin, flSpawnCooldownMax); - - // Randomize the array. - SortADTArray(hProxyCandidates, Sort_Random, Sort_Integer); - - decl Float:flDestinationPos[3]; - - for (new iNum = 0; iNum < iSpawnNum && iNum < iAvailableProxies; iNum++) - { - new iClient = GetArrayCell(hProxyCandidates, iNum); - new iBestAreaIndex = -1; - - if (GetArraySize(hAreaArrayClose) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayClose, GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1)); - } - else if (GetArraySize(hAreaArrayAverage) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayAverage, GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1)); - } - else if (GetArraySize(hAreaArrayFar) > 0) - { - iBestAreaIndex = GetArrayCell(hAreaArrayFar, GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1)); - } - - if (iBestAreaIndex == -1) - { -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any areas to place proxies (spawned %d)!", iBossIndex, iNum); -#endif - break; - } - - NavMeshArea_GetCenter(iBestAreaIndex, flDestinationPos); - - if (!GetConVarBool(g_cvPlayerProxyAsk)) - { - ClientStartProxyForce(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); - } - else - { - DisplayProxyAskMenu(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); - } - } - - CloseHandle(hAreaArrayClose); - CloseHandle(hAreaArrayAverage); - CloseHandle(hAreaArrayFar); - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d finished proxy process!", iBossIndex); -#endif - } - - CloseHandle(hProxyCandidates); - } - } - - return Plugin_Continue; -} - -ReloadRestrictedWeapons() -{ - if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) - { - CloseHandle(g_hRestrictedWeaponsConfig); - g_hRestrictedWeaponsConfig = INVALID_HANDLE; - } - - decl String:buffer[PLATFORM_MAX_PATH]; - BuildPath(Path_SM, buffer, sizeof(buffer), FILE_RESTRICTEDWEAPONS); - new Handle:kv = CreateKeyValues("root"); - if (!FileToKeyValues(kv, buffer)) - { - CloseHandle(kv); - LogError("Failed to load restricted weapons list! File not found!"); - } - else - { - g_hRestrictedWeaponsConfig = kv; - LogSF2Message("Reloaded restricted weapons configuration file successfully"); - } -} - -public Action:Timer_RoundMessages(Handle:timer) -{ - if (!g_bEnabled) return Plugin_Stop; - - if (timer != g_hRoundMessagesTimer) return Plugin_Stop; - - switch (g_iRoundMessagesNum) - { - case 0: CPrintToChatAll("{olive}==== {lightgreen}Slender Fortress (%s){olive} coded by {lightgreen}Kit o' Rifty{olive} ====", PLUGIN_VERSION_DISPLAY); - case 1: CPrintToChatAll("%t", "SF2 Ad Message 1"); - case 2: CPrintToChatAll("%t", "SF2 Ad Message 2"); - } - - g_iRoundMessagesNum++; - if (g_iRoundMessagesNum > 2) g_iRoundMessagesNum = 0; - - return Plugin_Continue; -} - -public Action:Timer_WelcomeMessage(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - CPrintToChat(client, "%T", "SF2 Welcome Message", client); -} - -GetMaxPlayersForRound() -{ - new iOverride = GetConVarInt(g_cvMaxPlayersOverride); - if (iOverride != -1) return iOverride; - return GetConVarInt(g_cvMaxPlayers); -} - -public OnConVarChanged(Handle:cvar, const String:oldValue[], const String:newValue[]) -{ - if (cvar == g_cvDifficulty) - { - switch (StringToInt(newValue)) - { - case Difficulty_Easy: g_flRoundDifficultyModifier = DIFFICULTY_EASY; - case Difficulty_Hard: g_flRoundDifficultyModifier = DIFFICULTY_HARD; - case Difficulty_Insane: g_flRoundDifficultyModifier = DIFFICULTY_INSANE; - default: g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; - } - } - else if (cvar == g_cvMaxPlayers || cvar == g_cvMaxPlayersOverride) - { - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - CheckPlayerGroup(i); - } - } - else if (cvar == g_cvPlayerShakeEnabled) - { - g_bPlayerShakeEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobEnabled) - { - g_bPlayerViewbobEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobHurtEnabled) - { - g_bPlayerViewbobHurtEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvPlayerViewbobSprintEnabled) - { - g_bPlayerViewbobSprintEnabled = bool:StringToInt(newValue); - } - else if (cvar == g_cvGravity) - { - g_flGravity = StringToFloat(newValue); - } - else if (cvar == g_cv20Dollars) - { - g_b20Dollars = bool:StringToInt(newValue); - } - else if (cvar == g_cvAllChat) - { - if (g_bEnabled) - { - for (new i = 1; i <= MaxClients; i++) - { - ClientUpdateListeningFlags(i); - } - } - } -} - -// ========================================================== -// IN-GAME AND ENTITY HOOK FUNCTIONS -// ========================================================== - - -public OnEntityCreated(ent, const String:classname[]) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(ent) || ent <= 0) return; - - if (StrEqual(classname, "spotlight_end", false)) - { - SDKHook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); - } - else if (StrEqual(classname, "beam", false)) - { - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightBeamSetTransmit); - } - - PvP_OnEntityCreated(ent, classname); -} - -public OnEntityDestroyed(ent) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(ent) || ent <= 0) return; - - decl String:sClassname[64]; - GetEntityClassname(ent, sClassname, sizeof(sClassname)); - - if (StrEqual(sClassname, "light_dynamic", false)) - { - AcceptEntityInput(ent, "TurnOff"); - - new iEnd = INVALID_ENT_REFERENCE; - while ((iEnd = FindEntityByClassname(iEnd, "spotlight_end")) != -1) - { - if (GetEntPropEnt(iEnd, Prop_Data, "m_hOwnerEntity") == ent) - { - AcceptEntityInput(iEnd, "Kill"); - break; - } - } - } - - PvP_OnEntityDestroyed(ent, sClassname); -} - -public Action:Hook_BlockUserMessage(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) -{ - if (!g_bEnabled) return Plugin_Continue; - return Plugin_Handled; -} - -public Action:Hook_NormalSound(clients[64], &numClients, String:sample[PLATFORM_MAX_PATH], &entity, &channel, &Float:volume, &level, &pitch, &flags) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsValidClient(entity)) - { - if (IsClientInGhostMode(entity)) - { - switch (channel) - { - case SNDCHAN_VOICE, SNDCHAN_WEAPON, SNDCHAN_ITEM, SNDCHAN_BODY: return Plugin_Handled; - } - } - else if (g_bPlayerProxy[entity]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[entity]); - if (iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - switch (channel) - { - case SNDCHAN_VOICE: - { - if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) - { - return Plugin_Handled; - } - } - } - } - } - else if (!g_bPlayerEliminated[entity]) - { - switch (channel) - { - case SNDCHAN_VOICE: - { - if (IsRoundInIntro()) return Plugin_Handled; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Voice)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDVOICE; - } - } - } - case SNDCHAN_BODY: - { - if (!StrContains(sample, "player/footsteps", false) || StrContains(sample, "step", false) != -1) - { - if (GetConVarBool(g_cvPlayerViewbobSprintEnabled) && IsClientReallySprinting(entity)) - { - // Viewpunch. - new Float:flPunchVelStep[3]; - - decl Float:flVelocity[3]; - GetEntPropVector(entity, Prop_Data, "m_vecAbsVelocity", flVelocity); - new Float:flSpeed = GetVectorLength(flVelocity); - - flPunchVelStep[0] = flSpeed / 300.0; - flPunchVelStep[1] = 0.0; - flPunchVelStep[2] = 0.0; - - ClientViewPunch(entity, flPunchVelStep); - } - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Footstep)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEP; - - if (IsClientSprinting(entity) && !(GetEntProp(entity, Prop_Send, "m_bDucking") || GetEntProp(entity, Prop_Send, "m_bDucked"))) - { - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEPLOUD; - } - } - } - } - } - case SNDCHAN_ITEM, SNDCHAN_WEAPON: - { - if (StrContains(sample, "impact", false) != -1 || StrContains(sample, "hit", false) != -1) - { - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Weapon)) - { - GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; - g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDWEAPON; - } - } - } - } - } - } - } - - new bool:bModified = false; - - for (new i = 0; i < numClients; i++) - { - new iClient = clients[i]; - if (IsValidClient(iClient) && IsPlayerAlive(iClient) && !IsClientInGhostMode(iClient)) - { - new bool:bCanHearSound = true; - - if (IsValidClient(entity) && entity != iClient) - { - if (!g_bPlayerEliminated[iClient]) - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (!g_bPlayerEliminated[entity] && !DidClientEscape(entity)) - { - bCanHearSound = false; - } - } - } - } - - if (!bCanHearSound) - { - bModified = true; - clients[i] = -1; - } - } - } - - if (bModified) return Plugin_Changed; - return Plugin_Continue; -} - -public MRESReturn:Hook_EntityShouldTransmit(thisPointer, Handle:hReturn, Handle:hParams) -{ - if (!g_bEnabled) return MRES_Ignored; - - if (IsValidClient(thisPointer)) - { - if (DoesClientHaveConstantGlow(thisPointer)) - { - DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. - return MRES_Supercede; - } - } - else - { - new iBossIndex = NPCGetFromEntIndex(thisPointer); - if (iBossIndex != -1) - { - DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. - return MRES_Supercede; - } - } - - return MRES_Ignored; -} - -public Hook_TriggerOnStartTouch(const String:output[], caller, activator, Float:delay) -{ - if (!g_bEnabled) return; - - if (!IsValidEntity(caller)) return; - - decl String:sName[64]; - GetEntPropString(caller, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrContains(sName, "sf2_escape_trigger", false) == 0) - { - if (IsRoundInEscapeObjective()) - { - if (IsValidClient(activator) && IsPlayerAlive(activator) && !IsClientInDeathCam(activator) && !g_bPlayerEliminated[activator] && !DidClientEscape(activator)) - { - ClientEscape(activator); - TeleportClientToEscapePoint(activator); - } - } - } - - PvP_OnTriggerStartTouch(caller, activator); -} - -public Hook_TriggerOnEndTouch(const String:sOutput[], caller, activator, Float:flDelay) -{ - if (!g_bEnabled) return; - - PvP_OnTriggerEndTouch(caller, activator); -} - -public Action:Hook_PageOnTakeDamage(page, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsValidClient(attacker)) - { - if (!g_bPlayerEliminated[attacker]) - { - if (damagetype & 0x80) // 0x80 == melee damage - { - CollectPage(page, attacker); - } - } - } - - return Plugin_Continue; -} - -static CollectPage(page, activator) -{ - SetPageCount(g_iPageCount + 1); - g_iPlayerPageCount[activator] += 1; - EmitSoundToAll(PAGE_GRABSOUND, activator, SNDCHAN_ITEM, SNDLEVEL_SCREAMING); - - // Gives points. Credit to the makers of VSH/FF2. - new Handle:hEvent = CreateEvent("player_escort_score", true); - SetEventInt(hEvent, "player", activator); - SetEventInt(hEvent, "points", 1); - FireEvent(hEvent); - - AcceptEntityInput(page, "FireUser1"); - AcceptEntityInput(page, "Kill"); -} - -// ========================================================== -// GENERIC CLIENT HOOKS AND FUNCTIONS -// ========================================================== - - -public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2]) -{ - if (!g_bEnabled) return Plugin_Continue; - - ClientDisableFakeLagCompensation(client); - - // Check impulse (block spraying and built-in flashlight) - switch (impulse) - { - case 100: - { - impulse = 0; - } - case 201: - { - if (IsClientInGhostMode(client)) - { - impulse = 0; - } - } - } - - for (new i = 0; i < MAX_BUTTONS; i++) - { - new button = (1 << i); - - if ((buttons & button)) - { - if (!(g_iPlayerLastButtons[client] & button)) - { - ClientOnButtonPress(client, button); - } - } - else if ((g_iPlayerLastButtons[client] & button)) - { - ClientOnButtonRelease(client, button); - } - } - - g_iPlayerLastButtons[client] = buttons; - - return Plugin_Continue; -} - - -public OnClientCookiesCached(client) -{ - if (!g_bEnabled) return; - - // Load our saved settings. - new String:sCookie[64]; - GetClientCookie(client, g_hCookie, sCookie, sizeof(sCookie)); - - g_iPlayerQueuePoints[client] = 0; - - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; - g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = true; - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; - g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = true; - - if (sCookie[0]) - { - new String:s2[12][32]; - new count = ExplodeString(sCookie, " ; ", s2, 12, 32); - - if (count > 0) - g_iPlayerQueuePoints[client] = StringToInt(s2[0]); - if (count > 1) - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = bool:StringToInt(s2[1]); - if (count > 2) - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode:StringToInt(s2[2]); - if (count > 3) - g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = bool:StringToInt(s2[3]); - if (count > 4) - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = bool:StringToInt(s2[4]); - if (count > 5) - g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = bool:StringToInt(s2[5]); - } -} - -public OnClientPutInServer(client) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientPutInServer(%d)", client); -#endif - - ClientSetPlayerGroup(client, -1); - - g_bPlayerEscaped[client] = false; - g_bPlayerEliminated[client] = true; - g_bPlayerChoseTeam[client] = false; - g_bPlayerPlayedSpecialRound[client] = true; - g_bPlayerPlayedNewBossRound[client] = true; - - g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn] = false; - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; - - g_iPlayerPageCount[client] = 0; - g_iPlayerDesiredFOV[client] = 90; - - SDKHook(client, SDKHook_PreThink, Hook_ClientPreThink); - SDKHook(client, SDKHook_SetTransmit, Hook_ClientSetTransmit); - SDKHook(client, SDKHook_OnTakeDamage, Hook_ClientOnTakeDamage); - - DHookEntity(g_hSDKWantsLagCompensationOnEntity, true, client); - DHookEntity(g_hSDKShouldTransmit, true, client); - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - - SetPlayerGroupInvitedPlayer(i, client, false); - SetPlayerGroupInvitedPlayerCount(i, client, 0); - SetPlayerGroupInvitedPlayerTime(i, client, 0.0); - } - - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientUpdateListeningFlags(client); - ClientUpdateMusicSystem(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - ClientResetProxy(client); - ClientResetHints(client); - ClientResetScare(client); - - ClientResetDeathCam(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientSetScareBoostEndTime(client, -1.0); - - ClientStartProxyAvailableTimer(client); - - if (!IsFakeClient(client)) - { - // See if the player is using the projected flashlight. - QueryClientConVar(client, "mat_supportflashlight", OnClientGetProjectedFlashlightSetting); - - // Get desired FOV. - QueryClientConVar(client, "fov_desired", OnClientGetDesiredFOV); - } - - PvP_OnClientPutInServer(client); - -#if defined DEBUG - g_iPlayerDebugFlags[client] = 0; - - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientPutInServer(%d)", client); -#endif -} - -public OnClientGetProjectedFlashlightSetting(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) -{ - if (result != ConVarQuery_Okay) - { - LogError("Warning: Player %N failed to query for ConVar mat_supportflashlight", client); - return; - } - - if (StringToInt(cvarValue)) - { - decl String:sAuth[64]; - GetClientAuthString(client, sAuth, sizeof(sAuth)); - - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = true; - LogSF2Message("Player %N (%s) has mat_supportflashlight enabled, projected flashlight will be used", client, sAuth); - } -} - -public OnClientGetDesiredFOV(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) -{ - if (!IsValidClient(client)) return; - - g_iPlayerDesiredFOV[client] = StringToInt(cvarValue); -} - -public OnClientDisconnect(client) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientDisconnect(%d)", client); -#endif - - g_bPlayerEscaped[client] = false; - - // Save and reset settings for the next client. - ClientSaveCookies(client); - ClientSetPlayerGroup(client, -1); - - // Reset variables. - g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; - g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; - g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = true; - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; - g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; - - // Reset any client functions that may be still active. - ClientResetOverlay(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientSetGhostModeState(client, false); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientStopProxyForce(client); - - if (!IsRoundInWarmup()) - { - if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) - { - if (g_bRoundGrace) - { - // Force the next player in queue to take my place, if any. - ForceInNextPlayersInQueue(1, true); - } - else - { - if (!IsRoundEnding()) - { - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - } - - // Reset queue points global variable. - g_iPlayerQueuePoints[client] = 0; - - PvP_OnClientDisconnect(client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientDisconnect(%d)", client); -#endif -} - -public OnClientDisconnect_Post(client) -{ - g_iPlayerLastButtons[client] = 0; -} - -public TF2_OnWaitingForPlayersStart() -{ - g_bRoundWaitingForPlayers = true; -} - -public TF2_OnWaitingForPlayersEnd() -{ - g_bRoundWaitingForPlayers = false; -} - -SF2RoundState:GetRoundState() -{ - return g_iRoundState; -} - -SetRoundState(SF2RoundState:iRoundState) -{ - if (g_iRoundState == iRoundState) return; - - PrintToServer("SetRoundState(%d)", iRoundState); - - new SF2RoundState:iOldRoundState = GetRoundState(); - g_iRoundState = iRoundState; - - // Cleanup from old roundstate if needed. - switch (iOldRoundState) - { - case SF2RoundState_Waiting: - { - } - case SF2RoundState_Intro: - { - g_hRoundIntroTimer = INVALID_HANDLE; - } - case SF2RoundState_Active: - { - g_bRoundGrace = false; - g_hRoundGraceTimer = INVALID_HANDLE; - g_hRoundTimer = INVALID_HANDLE; - } - case SF2RoundState_Escape: - { - g_hRoundTimer = INVALID_HANDLE; - } - case SF2RoundState_Outro: - { - } - } - - switch (g_iRoundState) - { - case SF2RoundState_Waiting: - { - } - case SF2RoundState_Intro: - { - g_hRoundIntroTimer = INVALID_HANDLE; - g_iRoundIntroText = 0; - g_bRoundIntroTextDefault = false; - g_hRoundIntroTextTimer = CreateTimer(0.0, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hRoundIntroTextTimer); - - // Gather data on the intro parameters set by the map. - new Float:flHoldTime = g_flRoundIntroFadeHoldTime; - g_hRoundIntroTimer = CreateTimer(flHoldTime, Timer_ActivateRoundFromIntro, _, TIMER_FLAG_NO_MAPCHANGE); - - // Trigger any intro logic entities, if any. - new ent = -1; - while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) - { - decl String:sName[64]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_intro_relay", false)) - { - AcceptEntityInput(ent, "Trigger"); - break; - } - } - } - case SF2RoundState_Active: - { - // Start the grace period timer. - g_bRoundGrace = true; - g_hRoundGraceTimer = CreateTimer(GetConVarFloat(g_cvGraceTime), Timer_RoundGrace, _, TIMER_FLAG_NO_MAPCHANGE); - - CreateTimer(2.0, Timer_RoundStart, _, TIMER_FLAG_NO_MAPCHANGE); - - // Enable movement on players. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - SetEntityFlags(i, GetEntityFlags(i) & ~FL_FROZEN); - } - - // Fade in. - new Float:flFadeTime = g_flRoundIntroFadeDuration; - new iFadeFlags = SF_FADE_IN | FFADE_PURGE; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - UTIL_ScreenFade(i, FixedUnsigned16(flFadeTime, 1 << 12), 0, iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); - } - } - case SF2RoundState_Escape: - { - // Initialize the escape timer, if needed. - if (g_iRoundEscapeTimeLimit > 0) - { - g_iRoundTime = g_iRoundEscapeTimeLimit; - g_hRoundTimer = CreateTimer(1.0, Timer_RoundTimeEscape, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hRoundTimer = INVALID_HANDLE; - } - - decl String:sName[32]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_logic_escape", false)) - { - AcceptEntityInput(ent, "FireUser1"); - break; - } - } - } - case SF2RoundState_Outro: - { - if (!g_bRoundHasEscapeObjective) - { - // Teleport winning players to the escape point. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - TeleportClientToEscapePoint(i); - } - } - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (IsClientInGhostMode(i)) - { - // Take the player out of ghost mode. - ClientSetGhostModeState(i, false); - TF2_RespawnPlayer(i); - } - else if (g_bPlayerProxy[i]) - { - TF2_RespawnPlayer(i); - } - - if (!g_bPlayerEliminated[i]) - { - // Give them back all their weapons so they can beat the crap out of the other team. - TF2_RegeneratePlayer(i); - } - - ClientUpdateListeningFlags(i); - } - } - } -} - -bool:IsRoundInEscapeObjective() -{ - return bool:(GetRoundState() == SF2RoundState_Escape); -} - -bool:IsRoundInWarmup() -{ - return bool:(GetRoundState() == SF2RoundState_Waiting); -} - -bool:IsRoundInIntro() -{ - return bool:(GetRoundState() == SF2RoundState_Intro); -} - -bool:IsRoundEnding() -{ - return bool:(GetRoundState() == SF2RoundState_Outro); -} - -bool:IsInfiniteBlinkEnabled() -{ - return bool:(g_bRoundInfiniteBlink || (GetConVarInt(g_cvPlayerInfiniteBlinkOverride) == 1)); -} - -bool:IsInfiniteFlashlightEnabled() -{ - return bool:(g_bRoundInfiniteFlashlight || (GetConVarInt(g_cvPlayerInfiniteFlashlightOverride) == 1)); -} - -bool:IsInfiniteSprintEnabled() -{ - return bool:(g_bRoundInfiniteSprint || (GetConVarInt(g_cvPlayerInfiniteSprintOverride) == 1)); -} - - -#define SF2_PLAYER_HUD_BLINK_SYMBOL "B" -#define SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL "ÏŸ" -#define SF2_PLAYER_HUD_BAR_SYMBOL "|" -#define SF2_PLAYER_HUD_BAR_MISSING_SYMBOL "" -#define SF2_PLAYER_HUD_INFINITY_SYMBOL "∞" -#define SF2_PLAYER_HUD_SPRINT_SYMBOL "»" - -public Action:Timer_ClientAverageUpdate(Handle:timer) -{ - if (timer != g_hClientAverageUpdateTimer) return Plugin_Stop; - - if (!g_bEnabled) return Plugin_Stop; - - if (IsRoundInWarmup() || IsRoundEnding()) return Plugin_Continue; - - // First, process through HUD stuff. - decl String:buffer[256]; - - static iHudColorHealthy[3] = { 150, 255, 150 }; - static iHudColorCritical[3] = { 255, 10, 10 }; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (IsPlayerAlive(i) && !IsClientInDeathCam(i)) - { - if (!g_bPlayerEliminated[i]) - { - if (DidClientEscape(i)) continue; - - new iMaxBars = 12; - new iBars = RoundToCeil(float(iMaxBars) * ClientGetBlinkMeter(i)); - if (iBars > iMaxBars) iBars = iMaxBars; - - Format(buffer, sizeof(buffer), "%s ", SF2_PLAYER_HUD_BLINK_SYMBOL); - - if (IsInfiniteBlinkEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - - if (!g_bSpecialRound || g_iSpecialRoundType != SPECIALROUND_LIGHTSOUT) - { - iBars = RoundToCeil(float(iMaxBars) * ClientGetFlashlightBatteryLife(i)); - if (iBars > iMaxBars) iBars = iMaxBars; - - decl String:sBuffer2[64]; - Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL); - StrCat(buffer, sizeof(buffer), sBuffer2); - - if (IsInfiniteFlashlightEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - } - - iBars = RoundToCeil(float(iMaxBars) * (float(ClientGetSprintPoints(i)) / 100.0)); - if (iBars > iMaxBars) iBars = iMaxBars; - - decl String:sBuffer2[64]; - Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_SPRINT_SYMBOL); - StrCat(buffer, sizeof(buffer), sBuffer2); - - if (IsInfiniteSprintEnabled()) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); - } - else - { - for (new i2 = 0; i2 < iMaxBars; i2++) - { - if (i2 < iBars) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - else - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); - } - } - } - - - new Float:flHealthRatio = float(GetEntProp(i, Prop_Send, "m_iHealth")) / float(SDKCall(g_hSDKGetMaxHealth, i)); - - new iColor[3]; - for (new i2 = 0; i2 < 3; i2++) - { - iColor[i2] = RoundFloat(float(iHudColorHealthy[i2]) + (float(iHudColorCritical[i2] - iHudColorHealthy[i2]) * (1.0 - flHealthRatio))); - } - - SetHudTextParams(0.035, 0.83, - 0.3, - iColor[0], - iColor[1], - iColor[2], - 40, - _, - 1.0, - 0.07, - 0.5); - ShowSyncHudText(i, g_hHudSync2, buffer); - } - else - { - if (g_bPlayerProxy[i]) - { - new iMaxBars = 12; - new iBars = RoundToCeil(float(iMaxBars) * (float(g_iPlayerProxyControl[i]) / 100.0)); - if (iBars > iMaxBars) iBars = iMaxBars; - - strcopy(buffer, sizeof(buffer), "CONTROL\n"); - - for (new i2 = 0; i2 < iBars; i2++) - { - StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); - } - - SetHudTextParams(-1.0, 0.83, - 0.3, - SF2_HUD_TEXT_COLOR_R, - SF2_HUD_TEXT_COLOR_G, - SF2_HUD_TEXT_COLOR_B, - 40, - _, - 1.0, - 0.07, - 0.5); - ShowSyncHudText(i, g_hHudSync2, buffer); - } - } - } - - ClientUpdateListeningFlags(i); - ClientUpdateMusicSystem(i); - } - - return Plugin_Continue; -} - -stock bool:IsClientParticipating(client) -{ - if (!IsValidClient(client)) return false; - - if (bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) - { - // Who would coach in this game? - return false; - } - - new iTeam = GetClientTeam(client); - - if (g_bPlayerLagCompensation[client]) - { - iTeam = g_iPlayerLagCompensationTeam[client]; - } - - switch (iTeam) - { - case TFTeam_Unassigned, TFTeam_Spectator: return false; - } - - if (_:TF2_GetPlayerClass(client) == 0) - { - // Player hasn't chosen a class? What. - return false; - } - - return true; -} - -Handle:GetQueueList() -{ - new Handle:hArray = CreateArray(3); - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientParticipating(i)) continue; - if (IsPlayerGroupActive(ClientGetPlayerGroup(i))) continue; - - new index = PushArrayCell(hArray, i); - SetArrayCell(hArray, index, g_iPlayerQueuePoints[i], 1); - SetArrayCell(hArray, index, false, 2); - } - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - new index = PushArrayCell(hArray, i); - SetArrayCell(hArray, index, GetPlayerGroupQueuePoints(i), 1); - SetArrayCell(hArray, index, true, 2); - } - - if (GetArraySize(hArray)) SortADTArrayCustom(hArray, SortQueueList); - return hArray; -} - -SetClientPlayState(client, bool:bState, bool:bEnablePlay=true) -{ - if (bState) - { - if (!g_bPlayerEliminated[client]) return; - - g_bPlayerEliminated[client] = false; - g_bPlayerPlaying[client] = bEnablePlay; - g_hPlayerSwitchBlueTimer[client] = INVALID_HANDLE; - - ClientSetGhostModeState(client, false); - - PvP_SetPlayerPvPState(client, false, false, false); - - if (g_bSpecialRound) - { - SetClientPlaySpecialRoundState(client, true); - } - - if (g_bNewBossRound) - { - SetClientPlayNewBossRoundState(client, true); - } - - if (TF2_GetPlayerClass(client) == TFClassType:0) - { - // Player hasn't chosen a class for some reason. Choose one for him. - TF2_SetPlayerClass(client, TFClassType:GetRandomInt(1, 9), true, true); - } - - ChangeClientTeamNoSuicide(client, _:TFTeam_Red); - } - else - { - if (g_bPlayerEliminated[client]) return; - - g_bPlayerEliminated[client] = true; - g_bPlayerPlaying[client] = false; - - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - } -} - -bool:DidClientPlayNewBossRound(client) -{ - return g_bPlayerPlayedNewBossRound[client]; -} - -SetClientPlayNewBossRoundState(client, bool:bState) -{ - g_bPlayerPlayedNewBossRound[client] = bState; -} - -bool:DidClientPlaySpecialRound(client) -{ - return g_bPlayerPlayedNewBossRound[client]; -} - -SetClientPlaySpecialRoundState(client, bool:bState) -{ - g_bPlayerPlayedSpecialRound[client] = bState; -} - -TeleportClientToEscapePoint(client) -{ - if (!IsClientInGame(client)) return; - - new ent = EntRefToEntIndex(g_iRoundEscapePointEntity); - if (ent && ent != -1) - { - decl Float:flPos[3], Float:flAng[3]; - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); - GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); - - TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); - AcceptEntityInput(ent, "FireUser1", client); - } -} - -ForceInNextPlayersInQueue(iAmount, bool:bShowMessage=false) -{ - // Grab the next person in line, or the next group in line if space allows. - new iAmountLeft = iAmount; - new Handle:hPlayers = CreateArray(); - new Handle:hArray = GetQueueList(); - - for (new i = 0, iSize = GetArraySize(hArray); i < iSize && iAmountLeft > 0; i++) - { - if (!GetArrayCell(hArray, i, 2)) - { - new iClient = GetArrayCell(hArray, i); - if (g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; - - PushArrayCell(hPlayers, iClient); - iAmountLeft--; - } - else - { - new iGroupIndex = GetArrayCell(hArray, i); - if (!IsPlayerGroupActive(iGroupIndex)) continue; - - new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); - if (iMemberCount <= iAmountLeft) - { - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient) || g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; - if (ClientGetPlayerGroup(iClient) == iGroupIndex) - { - PushArrayCell(hPlayers, iClient); - } - } - - SetPlayerGroupPlaying(iGroupIndex, true); - - iAmountLeft -= iMemberCount; - } - } - } - - CloseHandle(hArray); - - for (new i = 0, iSize = GetArraySize(hPlayers); i < iSize; i++) - { - new iClient = GetArrayCell(hPlayers, i); - ClientSetQueuePoints(iClient, 0); - SetClientPlayState(iClient, true); - - if (bShowMessage) CPrintToChat(iClient, "%T", "SF2 Force Play", iClient); - } - - CloseHandle(hPlayers); -} - -public SortQueueList(index1, index2, Handle:array, Handle:hndl) -{ - new iQueuePoints1 = GetArrayCell(array, index1, 1); - new iQueuePoints2 = GetArrayCell(array, index2, 1); - - if (iQueuePoints1 > iQueuePoints2) return -1; - else if (iQueuePoints1 == iQueuePoints2) return 0; - return 1; -} - -// ========================================================== -// GENERIC PAGE/BOSS HOOKS AND FUNCTIONS -// ========================================================== - -public Action:Hook_SlenderObjectSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) - { - if (!IsValidEdict(GetEntPropEnt(other, Prop_Send, "m_hObserverTarget"))) return Plugin_Handled; - } - - return Plugin_Continue; -} - -public Action:Timer_SlenderBlinkBossThink(Handle:timer, any:entref) -{ - new slender = EntRefToEntIndex(entref); - if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; - - new iBossIndex = NPCGetFromEntIndex(slender); - if (iBossIndex == -1) return Plugin_Stop; - - if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (NPCGetType(iBossIndex) == SF2BossType_Creeper) - { - new bool:bMove = false; - - if ((GetGameTime() - g_flSlenderLastKill[iBossIndex]) >= GetProfileFloat(sProfile, "kill_cooldown")) - { - if (PeopleCanSeeSlender(iBossIndex, false, false) && !PeopleCanSeeSlender(iBossIndex, true, SlenderUsesBlink(iBossIndex))) - { - new iBestPlayer = -1; - new Handle:hArray = CreateArray(); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInDeathCam(i) || g_bPlayerEliminated[i] || DidClientEscape(i) || IsClientInGhostMode(i) || !PlayerCanSeeSlender(i, iBossIndex, false, false)) continue; - PushArrayCell(hArray, i); - } - - if (GetArraySize(hArray)) - { - decl Float:flSlenderPos[3]; - SlenderGetAbsOrigin(iBossIndex, flSlenderPos); - - decl Float:flTempPos[3]; - new iTempPlayer = -1; - new Float:flTempDist = 16384.0; - for (new i = 0; i < GetArraySize(hArray); i++) - { - new iClient = GetArrayCell(hArray, i); - GetClientAbsOrigin(iClient, flTempPos); - if (GetVectorDistance(flTempPos, flSlenderPos) < flTempDist) - { - iTempPlayer = iClient; - flTempDist = GetVectorDistance(flTempPos, flSlenderPos); - } - } - - iBestPlayer = iTempPlayer; - } - - CloseHandle(hArray); - - decl Float:buffer[3]; - if (iBestPlayer != -1 && SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, buffer)) - { - bMove = true; - - decl Float:flAng[3], Float:flBuffer[3]; - decl Float:flSlenderPos[3], Float:flPos[3]; - GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flSlenderPos); - GetClientAbsOrigin(iBestPlayer, flPos); - SubtractVectors(flPos, buffer, flAng); - GetVectorAngles(flAng, flAng); - - // Take care of angle offsets. - AddVectors(flAng, g_flSlenderEyeAngOffset[iBossIndex], flAng); - for (new i = 0; i < 3; i++) flAng[i] = AngleNormalize(flAng[i]); - - flAng[0] = 0.0; - - // Take care of position offsets. - GetProfileVector(sProfile, "pos_offset", flBuffer); - AddVectors(buffer, flBuffer, buffer); - - TeleportEntity(slender, buffer, flAng, NULL_VECTOR); - - new Float:flMaxRange = GetProfileFloat(sProfile, "teleport_range_max"); - new Float:flDist = GetVectorDistance(buffer, flPos); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - - if (flDist < (flMaxRange * 0.33)) - { - GetProfileString(sProfile, "model_closedist", sBuffer, sizeof(sBuffer)); - } - else if (flDist < (flMaxRange * 0.66)) - { - GetProfileString(sProfile, "model_averagedist", sBuffer, sizeof(sBuffer)); - } - else - { - GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); - } - - // Fallback if error. - if (!sBuffer[0]) GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); - - SetEntProp(slender, Prop_Send, "m_nModelIndex", PrecacheModel(sBuffer)); - - if (flDist <= NPCGetInstantKillRadius(iBossIndex)) - { - if (NPCGetFlags(iBossIndex) & SFF_FAKE) - { - SlenderMarkAsFake(iBossIndex); - return Plugin_Stop; - } - else - { - g_flSlenderLastKill[iBossIndex] = GetGameTime(); - ClientStartDeathCam(iBestPlayer, iBossIndex, buffer); - } - } - } - } - } - - if (bMove) - { - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_move_single", sBuffer, sizeof(sBuffer)); - if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - - GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); - if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING, SND_CHANGEVOL); - } - else - { - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); - if (sBuffer[0]) StopSound(slender, SNDCHAN_AUTO, sBuffer); - } - } - - return Plugin_Continue; -} - - -SlenderOnClientStressUpdate(client) -{ - new Float:flStress = g_flPlayerStress[client]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) - { - if (NPCGetUniqueID(iBossIndex) == -1) continue; - - new iBossFlags = NPCGetFlags(iBossIndex); - if (iBossFlags & SFF_MARKEDASFAKE || - iBossFlags & SFF_NOTELEPORT) - { - continue; - } - - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); - if (iTeleportTarget && iTeleportTarget != INVALID_ENT_REFERENCE) - { - if (g_bPlayerEliminated[iTeleportTarget] || - DidClientEscape(iTeleportTarget) || - flStress >= g_flSlenderTeleportMaxTargetStress[iBossIndex] || - GetGameTime() >= g_flSlenderTeleportMaxTargetTime[iBossIndex]) - { - // Queue for a new target and mark the old target in the rest period. - new Float:flRestPeriod = GetProfileFloat(sProfile, "teleport_target_rest_period", 15.0); - flRestPeriod = (flRestPeriod * GetRandomFloat(0.92, 1.08)) / (NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier); - - g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; - g_flSlenderTeleportPlayersRestTime[iBossIndex][iTeleportTarget] = GetGameTime() + flRestPeriod; - g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; - g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: lost target, putting at rest period", iBossIndex); -#endif - } - } - else if (!g_bRoundGrace) - { - new iPreferredTeleportTarget = INVALID_ENT_REFERENCE; - - new Float:flTargetStressMin = GetProfileFloat(sProfile, "teleport_target_stress_min", 0.2); - new Float:flTargetStressMax = GetProfileFloat(sProfile, "teleport_target_stress_max", 0.9); - - new Float:flTargetStress = flTargetStressMax - ((flTargetStressMax - flTargetStressMin) / (g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex))); - - new Float:flPreferredTeleportTargetStress = flTargetStress; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || - !IsPlayerAlive(i) || - g_bPlayerEliminated[i] || - IsClientInGhostMode(i) || - DidClientEscape(i)) - { - continue; - } - - if (g_flPlayerStress[i] < flPreferredTeleportTargetStress) - { - if (g_flSlenderTeleportPlayersRestTime[iBossIndex][i] <= GetGameTime()) - { - iPreferredTeleportTarget = i; - flPreferredTeleportTargetStress = g_flPlayerStress[i]; - } - } - } - - if (iPreferredTeleportTarget && iPreferredTeleportTarget != INVALID_ENT_REFERENCE) - { - // Set our preferred target to the new guy. - new Float:flTargetDuration = GetProfileFloat(sProfile, "teleport_target_persistency_period", 13.0); - new Float:flDeviation = GetRandomFloat(0.92, 1.08); - flTargetDuration = Pow(flDeviation * flTargetDuration, ((g_flRoundDifficultyModifier * (NPCGetAnger(iBossIndex) - 1.0)) / 2.0)) + ((flDeviation * flTargetDuration) - 1.0); - - g_iSlenderTeleportTarget[iBossIndex] = EntIndexToEntRef(iPreferredTeleportTarget); - g_flSlenderTeleportPlayersRestTime[iBossIndex][iPreferredTeleportTarget] = -1.0; - g_flSlenderTeleportMaxTargetTime[iBossIndex] = GetGameTime() + flTargetDuration; - g_flSlenderTeleportTargetTime[iBossIndex] = GetGameTime(); - g_flSlenderTeleportMaxTargetStress[iBossIndex] = flTargetStress; - - iTeleportTarget = iPreferredTeleportTarget; - -#if defined DEBUG - SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: got new target %N", iBossIndex, iPreferredTeleportTarget); -#endif - } - } - } -} - -static GetPageMusicRanges() -{ - ClearArray(g_hPageMusicRanges); - - decl String:sName[64]; - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (sName[0] && !StrContains(sName, "sf2_page_music_", false)) - { - ReplaceString(sName, sizeof(sName), "sf2_page_music_", "", false); - - new String:sPageRanges[2][32]; - ExplodeString(sName, "-", sPageRanges, 2, 32); - - new iIndex = PushArrayCell(g_hPageMusicRanges, EntIndexToEntRef(ent)); - if (iIndex != -1) - { - new iMin = StringToInt(sPageRanges[0]); - new iMax = StringToInt(sPageRanges[1]); - -#if defined DEBUG - DebugMessage("Page range found: entity %d, iMin = %d, iMax = %d", ent, iMin, iMax); -#endif - SetArrayCell(g_hPageMusicRanges, iIndex, iMin, 1); - SetArrayCell(g_hPageMusicRanges, iIndex, iMax, 2); - } - } - } - - // precache - if (GetArraySize(g_hPageMusicRanges) > 0) - { - decl String:sPath[PLATFORM_MAX_PATH]; - - for (new i = 0; i < GetArraySize(g_hPageMusicRanges); i++) - { - ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - PrecacheSound(sPath); - } - } - } - - LogSF2Message("Loaded page music ranges successfully!"); -} - -SetPageCount(iNum) -{ - if (iNum > g_iPageMax) iNum = g_iPageMax; - - new iOldPageCount = g_iPageCount; - g_iPageCount = iNum; - - if (g_iPageCount != iOldPageCount) - { - if (g_iPageCount > iOldPageCount) - { - if (g_hRoundGraceTimer != INVALID_HANDLE) - { - TriggerTimer(g_hRoundGraceTimer); - } - - g_iRoundTime += g_iRoundTimeGainFromPage; - if (g_iRoundTime > g_iRoundTimeLimit) g_iRoundTime = g_iRoundTimeLimit; - - // Increase anger on selected bosses. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new Float:flPageDiff = NPCGetAngerAddOnPageGrabTimeDiff(i); - if (flPageDiff >= 0.0) - { - new iDiff = g_iPageCount - iOldPageCount; - if ((GetGameTime() - g_flPageFoundLastTime) < flPageDiff) - { - NPCAddAnger(i, NPCGetAngerAddOnPageGrab(i) * float(iDiff)); - } - } - } - - g_flPageFoundLastTime = GetGameTime(); - } - - // Notify logic entities. - decl String:sTargetName[64]; - decl String:sFindTargetName[64]; - Format(sFindTargetName, sizeof(sFindTargetName), "sf2_onpagecount_%d", g_iPageCount); - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); - if (sTargetName[0] && StrEqual(sTargetName, sFindTargetName, false)) - { - AcceptEntityInput(ent, "Trigger"); - break; - } - } - - new iClients[MAXPLAYERS + 1] = { -1, ... }; - new iClientsNum = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (!g_bPlayerEliminated[i] || IsClientInGhostMode(i)) - { - if (g_iPageCount) - { - iClients[iClientsNum] = i; - iClientsNum++; - } - } - } - - if (g_iPageCount > 0 && g_bRoundHasEscapeObjective && g_iPageCount == g_iPageMax) - { - // Escape initialized! - SetRoundState(SF2RoundState_Escape); - - if (iClientsNum) - { - new iGameTextEscape = GetTextEntity("sf2_escape_message", false); - if (iGameTextEscape != -1) - { - // Custom escape message. - decl String:sMessage[512]; - GetEntPropString(iGameTextEscape, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextEscape, g_hHudSync, sMessage); - } - else - { - // Default escape message. - for (new i = 0; i < iClientsNum; i++) - { - new client = iClients[i]; - ClientShowMainMessage(client, "%d/%d\n%T", g_iPageCount, g_iPageMax, "SF2 Default Escape Message", i); - } - } - } - } - else - { - if (iClientsNum) - { - new iGameTextPage = GetTextEntity("sf2_page_message", false); - if (iGameTextPage != -1) - { - // Custom page message. - decl String:sMessage[512]; - GetEntPropString(iGameTextPage, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextPage, g_hHudSync, sMessage, g_iPageCount, g_iPageMax); - } - else - { - // Default page message. - for (new i = 0; i < iClientsNum; i++) - { - new client = iClients[i]; - ClientShowMainMessage(client, "%d/%d", g_iPageCount, g_iPageMax); - } - } - } - } - - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - } -} - -GetTextEntity(const String:sTargetName[], bool:bCaseSensitive=true) -{ - // Try to see if we can use a custom message instead of the default. - decl String:targetName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "game_text")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (StrEqual(targetName, sTargetName, bCaseSensitive)) - { - return ent; - } - } - } - - return -1; -} - -ShowHudTextUsingTextEntity(const iClients[], iClientsNum, iGameText, Handle:hHudSync, const String:sMessage[], ...) -{ - if (!sMessage[0]) return; - if (!IsValidEntity(iGameText)) return; - - decl String:sTrueMessage[512]; - VFormat(sTrueMessage, sizeof(sTrueMessage), sMessage, 6); - - new Float:flX = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.x"); - new Float:flY = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.y"); - new iEffect = GetEntProp(iGameText, Prop_Data, "m_textParms.effect"); - new Float:flFadeInTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime"); - new Float:flFadeOutTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime"); - new Float:flHoldTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); - new Float:flFxTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fxTime"); - - new Color1[4] = { 255, 255, 255, 255 }; - new Color2[4] = { 255, 255, 255, 255 }; - - new iParmsOffset = FindDataMapOffs(iGameText, "m_textParms"); - if (iParmsOffset != -1) - { - // hudtextparms_s m_textParms - - Color1[0] = GetEntData(iGameText, iParmsOffset + 12, 1); - Color1[1] = GetEntData(iGameText, iParmsOffset + 13, 1); - Color1[2] = GetEntData(iGameText, iParmsOffset + 14, 1); - Color1[3] = GetEntData(iGameText, iParmsOffset + 15, 1); - - Color2[0] = GetEntData(iGameText, iParmsOffset + 16, 1); - Color2[1] = GetEntData(iGameText, iParmsOffset + 17, 1); - Color2[2] = GetEntData(iGameText, iParmsOffset + 18, 1); - Color2[3] = GetEntData(iGameText, iParmsOffset + 19, 1); - } - - SetHudTextParamsEx(flX, flY, flHoldTime, Color1, Color2, iEffect, flFxTime, flFadeInTime, flFadeOutTime); - - for (new i = 0; i < iClientsNum; i++) - { - new iClient = iClients[i]; - if (!IsValidClient(iClient) || IsFakeClient(iClient)) continue; - - ShowSyncHudText(iClient, hHudSync, sTrueMessage); - } -} - -// ========================================================== -// EVENT HOOKS -// ========================================================== - -public Event_RoundStart(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundStart"); -#endif - - // Reset some global variables. - g_iRoundCount++; - g_hRoundTimer = INVALID_HANDLE; - - SetRoundState(SF2RoundState_Invalid); - - SetPageCount(0); - g_iPageMax = 0; - g_flPageFoundLastTime = GetGameTime(); - - g_hVoteTimer = INVALID_HANDLE; - - // Remove all bosses from the game. - NPCRemoveAll(); - - // Refresh groups. - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - SetPlayerGroupPlaying(i, false); - CheckPlayerGroup(i); - } - - // Refresh players. - for (new i = 1; i <= MaxClients; i++) - { - ClientSetGhostModeState(i, false); - - g_bPlayerPlaying[i] = false; - g_bPlayerEliminated[i] = true; - g_bPlayerEscaped[i] = false; - } - - // Calculate the new round state. - if (g_bRoundWaitingForPlayers) - { - SetRoundState(SF2RoundState_Waiting); - } - else if (GetConVarBool(g_cvWarmupRound) && g_iRoundWarmupRoundCount < GetConVarInt(g_cvWarmupRoundNum)) - { - g_iRoundWarmupRoundCount++; - - SetRoundState(SF2RoundState_Waiting); - - ServerCommand("mp_restartgame 15"); - PrintCenterTextAll("Round restarting in 15 seconds"); - } - else - { - g_iRoundActiveCount++; - - InitializeNewGame(); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundStart"); -#endif -} - -public Event_RoundEnd(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundEnd"); -#endif - - SetRoundState(SF2RoundState_Outro); - - DistributeQueuePointsToPlayers(); - - g_iRoundEndCount++; - CheckRoundLimitForBossPackVote(g_iRoundEndCount); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundEnd"); -#endif -} - -static DistributeQueuePointsToPlayers() -{ - // Give away queue points. - new iDefaultAmount = 5; - new iAmount = iDefaultAmount; - new iAmount2 = iAmount; - new Action:iAction = Plugin_Continue; - - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - - if (IsPlayerGroupPlaying(i)) - { - SetPlayerGroupQueuePoints(i, 0); - } - else - { - iAmount = iDefaultAmount; - iAmount2 = iAmount; - iAction = Plugin_Continue; - - Call_StartForward(fOnGroupGiveQueuePoints); - Call_PushCell(i); - Call_PushCellRef(iAmount2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) iAmount = iAmount2; - - SetPlayerGroupQueuePoints(i, GetPlayerGroupQueuePoints(i) + iAmount); - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsValidClient(iClient)) continue; - if (ClientGetPlayerGroup(iClient) == i) - { - CPrintToChat(iClient, "%T", "SF2 Give Group Queue Points", iClient, iAmount); - } - } - } - } - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (g_bPlayerPlaying[i]) - { - ClientSetQueuePoints(i, 0); - } - else - { - if (!IsClientParticipating(i)) - { - CPrintToChat(i, "%T", "SF2 No Queue Points To Spectator", i); - } - else - { - iAmount = iDefaultAmount; - iAmount2 = iAmount; - iAction = Plugin_Continue; - - Call_StartForward(fOnClientGiveQueuePoints); - Call_PushCell(i); - Call_PushCellRef(iAmount2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) iAmount = iAmount2; - - ClientSetQueuePoints(i, g_iPlayerQueuePoints[i] + iAmount); - CPrintToChat(i, "%T", "SF2 Give Queue Points", i, iAmount); - } - } - } -} - -public Action:Event_PlayerTeamPre(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerTeamPre"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - if (GetEventInt(event, "team") > 1 || GetEventInt(event, "oldteam") > 1) SetEventBroadcast(event, true); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerTeamPre"); -#endif - - return Plugin_Continue; -} - -public Event_PlayerTeam(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerTeam"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - new iNewTeam = GetEventInt(event, "team"); - if (iNewTeam <= _:TFTeam_Spectator) - { - if (g_bRoundGrace) - { - if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) - { - ForceInNextPlayersInQueue(1, true); - } - } - - // You're not playing anymore. - if (g_bPlayerPlaying[client]) - { - ClientSetQueuePoints(client, 0); - } - - g_bPlayerPlaying[client] = false; - g_bPlayerEliminated[client] = true; - g_bPlayerEscaped[client] = false; - - ClientSetGhostModeState(client, false); - - if (!bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) - { - // This is to prevent player spawn spam when someone is coaching. Who coaches in SF2, anyway? - TF2_RespawnPlayer(client); - } - - // Special round. - if (g_bSpecialRound) g_bPlayerPlayedSpecialRound[client] = true; - - // Boss round. - if (g_bNewBossRound) g_bPlayerPlayedNewBossRound[client] = true; - } - else - { - if (!g_bPlayerChoseTeam[client]) - { - g_bPlayerChoseTeam[client] = true; - - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - EmitSoundToClient(client, SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); - CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Projected{olive}."); - } - else - { - CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Normal{olive}."); - } - - CreateTimer(5.0, Timer_WelcomeMessage, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - } - - // Check groups. - if (!IsRoundEnding()) - { - for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) - { - if (!IsPlayerGroupActive(i)) continue; - CheckPlayerGroup(i); - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerTeam"); -#endif - -} - -/** - * Sets the player to the correct team if needed. Returns true if a change was necessary, false if no change occurred. - */ -static bool:HandlePlayerTeam(client, bool:bRespawn=true) -{ - if (!IsClientInGame(client) || !IsClientParticipating(client)) return false; - - if (!g_bPlayerEliminated[client]) - { - if (GetClientTeam(client) != _:TFTeam_Red) - { - if (bRespawn) - ChangeClientTeamNoSuicide(client, _:TFTeam_Red); - else - ChangeClientTeam(client, _:TFTeam_Red); - - return true; - } - } - else - { - if (GetClientTeam(client) != _:TFTeam_Blue) - { - if (bRespawn) - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - else - ChangeClientTeam(client, _:TFTeam_Blue); - - return true; - } - } - - return false; -} - -static HandlePlayerIntroState(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client) || !IsClientParticipating(client)) return; - - if (!IsRoundInIntro()) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START HandlePlayerIntroState(%d)", client); -#endif - - // Disable movement on player. - SetEntityFlags(client, GetEntityFlags(client) | FL_FROZEN); - - new Float:flDelay = 0.0; - if (!IsFakeClient(client)) - { - flDelay = GetClientLatency(client, NetFlow_Outgoing); - } - - CreateTimer(flDelay * 4.0, Timer_IntroBlackOut, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END HandlePlayerIntroState(%d)", client); -#endif -} - -HandlePlayerHUD(client) -{ - if (IsRoundInWarmup() || IsClientInGhostMode(client)) - { - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - else - { - if (!g_bPlayerEliminated[client]) - { - if (!DidClientEscape(client)) - { - // Player is in the game; disable normal HUD. - SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); - } - else - { - // Player isn't in the game; enable normal HUD behavior. - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - } - else - { - if (g_bPlayerProxy[client]) - { - // Player is in the game; disable normal HUD. - SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); - } - else - { - // Player isn't in the game; enable normal HUD behavior. - SetEntProp(client, Prop_Send, "m_iHideHUD", 0); - } - } - } -} - -public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client <= 0) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerSpawn(%d)", client); -#endif - - if (!IsClientParticipating(client)) - { - ClientSetGhostModeState(client, false); - } - - g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; - - if (IsPlayerAlive(client) && IsClientParticipating(client)) - { - if (HandlePlayerTeam(client)) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("client->HandlePlayerTeam()"); -#endif - } - else - { - g_iPlayerPageCount[client] = 0; - - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientUpdateListeningFlags(client); - ClientUpdateMusicSystem(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - ClientResetProxy(client); - ClientResetHints(client); - ClientResetScare(client); - - ClientResetDeathCam(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - - ClientHandleGhostMode(client); - - if (!g_bPlayerEliminated[client]) - { - ClientStartDrainingBlinkMeter(client); - ClientSetScareBoostEndTime(client, -1.0); - - ClientStartCampingTimer(client); - - HandlePlayerIntroState(client); - - // screen overlay timer - g_hPlayerOverlayCheck[client] = CreateTimer(0.0, Timer_PlayerOverlayCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerOverlayCheck[client], true); - - if (DidClientEscape(client)) - { - CreateTimer(0.1, Timer_TeleportPlayerToEscapePoint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - ClientEnableConstantGlow(client, "head"); - ClientActivateUltravision(client); - } - } - else - { - g_hPlayerOverlayCheck[client] = INVALID_HANDLE; - } - - g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - HandlePlayerHUD(client); - } - } - - PvP_OnPlayerSpawn(client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerSpawn(%d)", client); -#endif -} - -public Action:Timer_IntroBlackOut(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!IsRoundInIntro()) return; - - if (!IsPlayerAlive(client) || g_bPlayerEliminated[client]) return; - - // Black out the player's screen. - new iFadeFlags = FFADE_OUT | FFADE_STAYOUT | FFADE_PURGE; - UTIL_ScreenFade(client, 0, FixedUnsigned16(90.0, 1 << 12), iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); -} - -public Event_PostInventoryApplication(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PostInventoryApplication"); -#endif - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PostInventoryApplication"); -#endif -} - -public Action:Event_DontBroadcastToClients(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - if (IsRoundInWarmup()) return Plugin_Continue; - - SetEventBroadcast(event, true); - return Plugin_Continue; -} - -public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return Plugin_Continue; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerDeathPre"); -#endif - - if (!IsRoundInWarmup()) - { - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client > 0) - { - if (!IsRoundEnding()) - { - if (g_bRoundGrace || g_bPlayerEliminated[client] || IsClientInGhostMode(client)) - { - SetEventBroadcast(event, true); - } - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerDeathPre"); -#endif - - return Plugin_Continue; -} - -public Event_PlayerHurt(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client <= 0) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerHurt"); -#endif - - ClientDisableFakeLagCompensation(client); - - new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); - if (attacker > 0) - { - if (g_bPlayerProxy[attacker]) - { - g_iPlayerProxyControl[attacker] = 100; - } - } - - // Play any sounds, if any. - if (g_bPlayerProxy[client]) - { - new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iProxyMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - if (GetRandomStringFromProfile(sProfile, "sound_proxy_hurt", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_hurt_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_hurt_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_hurt_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_hurt_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_hurt_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerHurt"); -#endif -} - -public Event_PlayerDeath(Handle:event, const String:name[], bool:dB) -{ - if (!g_bEnabled) return; - - new client = GetClientOfUserId(GetEventInt(event, "userid")); - if (client <= 0) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerDeath(%d)", client); -#endif - - new bool:bFake = bool:(GetEventInt(event, "death_flags") & TF_DEATHFLAG_DEADRINGER); - new inflictor = GetEventInt(event, "inflictor_entindex"); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("inflictor = %d", inflictor); -#endif - - if (!bFake) - { - ClientDisableFakeLagCompensation(client); - - ClientResetStatic(client); - ClientResetSlenderStats(client); - ClientResetCampingStats(client); - ClientResetOverlay(client); - ClientResetJumpScare(client); - ClientResetInteractiveGlow(client); - ClientDisableConstantGlow(client); - ClientChaseMusicReset(client); - ClientChaseMusicSeeReset(client); - ClientAlertMusicReset(client); - Client20DollarsMusicReset(client); - ClientMusicReset(client); - - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientResetSprint(client); - ClientResetBreathing(client); - ClientResetBlink(client); - ClientResetDeathCam(client); - - ClientUpdateMusicSystem(client); - - PvP_SetPlayerPvPState(client, false, false, false); - - if (IsRoundInWarmup()) - { - CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - if (!g_bPlayerEliminated[client]) - { - if (IsRoundInIntro() || g_bRoundGrace || DidClientEscape(client)) - { - CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_bPlayerEliminated[client] = true; - g_bPlayerEscaped[client] = false; - g_hPlayerSwitchBlueTimer[client] = CreateTimer(0.5, Timer_PlayerSwitchToBlue, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - } - else - { - } - - { - // If this player was killed by a boss, play a sound. - new npcIndex = NPCGetFromEntIndex(inflictor); - if (npcIndex != -1) - { - decl String:npcProfile[SF2_MAX_PROFILE_NAME_LENGTH], String:buffer[PLATFORM_MAX_PATH]; - NPCGetProfile(npcIndex, npcProfile, sizeof(npcProfile)); - - if (GetRandomStringFromProfile(npcProfile, "sound_attack_killed_all", buffer, sizeof(buffer)) && strlen(buffer) > 0) - { - if (!g_bPlayerEliminated[client]) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_HELICOPTER); - } - } - - SlenderPerformVoice(npcIndex, "sound_attack_killed"); - } - } - - CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); - - // Notify to other bosses that this player has died. - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (EntRefToEntIndex(g_iSlenderTarget[i]) == client) - { - g_iSlenderInterruptConditions[i] |= COND_CHASETARGETINVALIDATED; - GetClientAbsOrigin(client, g_flSlenderChaseDeathPosition[i]); - } - } - } - - if (g_bPlayerProxy[client]) - { - // We're a proxy, so play some sounds. - - new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - if (iProxyMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - if (GetRandomStringFromProfile(sProfile, "sound_proxy_death", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_death_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_death_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_death_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_death_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_death_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } - - ClientResetProxy(client, false); - ClientUpdateListeningFlags(client); - - // Half-Zatoichi nerf code. - new iKatanaHealthGain = GetConVarInt(g_cvHalfZatoichiHealthGain); - if (iKatanaHealthGain >= 0) - { - new iAttacker = GetClientOfUserId(GetEventInt(event, "attacker")); - if (iAttacker > 0) - { - if (!IsClientInPvP(iAttacker) && (!g_bPlayerEliminated[iAttacker] || g_bPlayerProxy[iAttacker])) - { - decl String:sWeapon[64]; - GetEventString(event, "weapon", sWeapon, sizeof(sWeapon)); - - if (StrEqual(sWeapon, "demokatana")) - { - new iAttackerPreHealth = GetEntProp(iAttacker, Prop_Send, "m_iHealth"); - new Handle:hPack = CreateDataPack(); - WritePackCell(hPack, GetClientUserId(iAttacker)); - WritePackCell(hPack, iAttackerPreHealth + iKatanaHealthGain); - - CreateTimer(0.0, Timer_SetPlayerHealth, hPack, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - } - - g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; - } - - PvP_OnPlayerDeath(client, bFake); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerDeath(%d)", client); -#endif -} - -public Action:Timer_SetPlayerHealth(Handle:timer, any:data) -{ - new Handle:hPack = Handle:data; - ResetPack(hPack); - new iAttacker = GetClientOfUserId(ReadPackCell(hPack)); - new iHealth = ReadPackCell(hPack); - CloseHandle(hPack); - - if (iAttacker <= 0) return; - - SetEntProp(iAttacker, Prop_Data, "m_iHealth", iHealth); - SetEntProp(iAttacker, Prop_Send, "m_iHealth", iHealth); -} - -public Action:Timer_PlayerSwitchToBlue(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSwitchBlueTimer[client]) return; - - ChangeClientTeam(client, _:TFTeam_Blue); -} - -public Action:Timer_RoundStart(Handle:timer) -{ - if (g_iPageMax > 0) - { - new Handle:hArrayClients = CreateArray(); - new iClients[MAXPLAYERS + 1]; - new iClientsNum = 0; - - new iGameText = GetTextEntity("sf2_intro_message", false); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || g_bPlayerEliminated[i]) continue; - - if (iGameText == -1) - { - if (g_iPageMax > 1) - { - ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Plural", i, g_iPageMax); - } - else - { - ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Singular", i, g_iPageMax); - } - } - - PushArrayCell(hArrayClients, GetClientUserId(i)); - iClients[iClientsNum] = i; - iClientsNum++; - } - - // Show difficulty menu. - if (iClientsNum) - { - // Automatically set it to Normal. - SetConVarInt(g_cvDifficulty, Difficulty_Normal); - - g_hVoteTimer = CreateTimer(1.0, Timer_VoteDifficulty, hArrayClients, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hVoteTimer, true); - - if (iGameText != -1) - { - decl String:sMessage[512]; - GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); - } - } - else - { - CloseHandle(hArrayClients); - } - } -} - -public Action:Timer_CheckRoundWinConditions(Handle:timer) -{ - CheckRoundWinConditions(); -} - -public Action:Timer_RoundGrace(Handle:timer) -{ - if (timer != g_hRoundGraceTimer) return; - - g_bRoundGrace = false; - g_hRoundGraceTimer = INVALID_HANDLE; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientParticipating(i)) g_bPlayerEliminated[i] = true; - } - - // Initialize the main round timer. - if (g_iRoundTimeLimit > 0) - { - // Set round time. - g_iRoundTime = g_iRoundTimeLimit; - g_hRoundTimer = CreateTimer(1.0, Timer_RoundTime, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - else - { - // Infinite round time. - g_hRoundTimer = INVALID_HANDLE; - } - - CPrintToChatAll("{olive}%t", "SF2 Grace Period End"); -} - -public Action:Timer_RoundTime(Handle:timer) -{ - if (timer != g_hRoundTimer) return Plugin_Stop; - - if (g_iRoundTime <= 0) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; - - decl Float:flBuffer[3]; - GetClientAbsOrigin(i, flBuffer); - SDKHooks_TakeDamage(i, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - } - - return Plugin_Stop; - } - - g_iRoundTime--; - - new hours, minutes, seconds; - FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); - - SetHudTextParams(-1.0, 0.1, - 1.0, - SF2_HUD_TEXT_COLOR_R, SF2_HUD_TEXT_COLOR_G, SF2_HUD_TEXT_COLOR_B, SF2_HUD_TEXT_COLOR_A, - _, - _, - 1.5, 1.5); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; - ShowSyncHudText(i, g_hRoundTimerSync, "%d/%d\n%d:%02d", g_iPageCount, g_iPageMax, minutes, seconds); - } - - return Plugin_Continue; -} - -public Action:Timer_RoundTimeEscape(Handle:timer) -{ - if (timer != g_hRoundTimer) return Plugin_Stop; - - if (g_iRoundTime <= 0) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i) || DidClientEscape(i)) continue; - - decl Float:flBuffer[3]; - GetClientAbsOrigin(i, flBuffer); - ClientStartDeathCam(i, 0, flBuffer); - } - - return Plugin_Stop; - } - - new hours, minutes, seconds; - FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); - - SetHudTextParams(-1.0, 0.1, - 1.0, - SF2_HUD_TEXT_COLOR_R, - SF2_HUD_TEXT_COLOR_G, - SF2_HUD_TEXT_COLOR_B, - SF2_HUD_TEXT_COLOR_A, - _, - _, - 1.5, 1.5); - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; - ShowSyncHudText(i, g_hRoundTimerSync, "%T\n%d:%02d", "SF2 Default Escape Message", i, minutes, seconds); - } - - g_iRoundTime--; - - return Plugin_Continue; -} - -public Action:Timer_VoteDifficulty(Handle:timer, any:data) -{ - new Handle:hArrayClients = Handle:data; - - if (timer != g_hVoteTimer || IsRoundEnding()) - { - CloseHandle(hArrayClients); - return Plugin_Stop; - } - - if (IsVoteInProgress()) return Plugin_Continue; // There's another vote in progess. Wait. - - new iClients[MAXPLAYERS + 1] = { -1, ... }; - new iClientsNum; - for (new i = 0, iSize = GetArraySize(hArrayClients); i < iSize; i++) - { - new iClient = GetClientOfUserId(GetArrayCell(hArrayClients, i)); - if (iClient <= 0) continue; - - iClients[iClientsNum] = iClient; - iClientsNum++; - } - - CloseHandle(hArrayClients); - - VoteMenu(g_hMenuVoteDifficulty, iClients, iClientsNum, 15); - - return Plugin_Stop; -} - -static InitializeMapEntities() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeMapEntities()"); -#endif - - g_bRoundInfiniteFlashlight = false; - g_bRoundInfiniteBlink = false; - g_bRoundInfiniteSprint = false; - g_bRoundHasEscapeObjective = false; - - g_iRoundTimeLimit = GetConVarInt(g_cvTimeLimit); - g_iRoundEscapeTimeLimit = GetConVarInt(g_cvTimeLimitEscape); - g_iRoundTimeGainFromPage = GetConVarInt(g_cvTimeGainFromPageGrab); - - // Reset page reference. - g_bPageRef = false; - strcopy(g_strPageRefModel, sizeof(g_strPageRefModel), ""); - g_flPageRefModelScale = 1.0; - - new Handle:hArray = CreateArray(2); - new Handle:hPageTrie = CreateTrie(); - - decl String:targetName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (!StrContains(targetName, "sf2_maxpages_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_maxpages_", "", false); - g_iPageMax = StringToInt(targetName); - } - else if (!StrContains(targetName, "sf2_page_spawnpoint", false)) - { - if (!StrContains(targetName, "sf2_page_spawnpoint_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_page_spawnpoint_", "", false); - if (targetName[0]) - { - new Handle:hButtStallion = INVALID_HANDLE; - if (!GetTrieValue(hPageTrie, targetName, hButtStallion)) - { - hButtStallion = CreateArray(); - SetTrieValue(hPageTrie, targetName, hButtStallion); - } - - new iIndex = FindValueInArray(hArray, hButtStallion); - if (iIndex == -1) - { - iIndex = PushArrayCell(hArray, hButtStallion); - } - - PushArrayCell(hButtStallion, ent); - SetArrayCell(hArray, iIndex, true, 1); - } - else - { - new iIndex = PushArrayCell(hArray, ent); - SetArrayCell(hArray, iIndex, false, 1); - } - } - else - { - new iIndex = PushArrayCell(hArray, ent); - SetArrayCell(hArray, iIndex, false, 1); - } - } - else if (!StrContains(targetName, "sf2_logic_escape", false)) - { - g_bRoundHasEscapeObjective = true; - } - else if (!StrContains(targetName, "sf2_infiniteflashlight", false)) - { - g_bRoundInfiniteFlashlight = true; - } - else if (!StrContains(targetName, "sf2_infiniteblink", false)) - { - g_bRoundInfiniteBlink = true; - } - else if (!StrContains(targetName, "sf2_infinitesprint", false)) - { - g_bRoundInfiniteSprint = true; - } - else if (!StrContains(targetName, "sf2_time_limit_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_time_limit_", "", false); - g_iRoundTimeLimit = StringToInt(targetName); - - LogSF2Message("Found sf2_time_limit entity, set time limit to %d", g_iRoundTimeLimit); - } - else if (!StrContains(targetName, "sf2_escape_time_limit_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_escape_time_limit_", "", false); - g_iRoundEscapeTimeLimit = StringToInt(targetName); - - LogSF2Message("Found sf2_escape_time_limit entity, set escape time limit to %d", g_iRoundEscapeTimeLimit); - } - else if (!StrContains(targetName, "sf2_time_gain_from_page_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_time_gain_from_page_", "", false); - g_iRoundTimeGainFromPage = StringToInt(targetName); - - LogSF2Message("Found sf2_time_gain_from_page entity, set time gain to %d", g_iRoundTimeGainFromPage); - } - else if (g_iRoundActiveCount == 1 && (!StrContains(targetName, "sf2_maxplayers_", false))) - { - ReplaceString(targetName, sizeof(targetName), "sf2_maxplayers_", "", false); - SetConVarInt(g_cvMaxPlayers, StringToInt(targetName)); - - LogSF2Message("Found sf2_maxplayers entity, set maxplayers to %d", StringToInt(targetName)); - } - else if (!StrContains(targetName, "sf2_boss_override_", false)) - { - ReplaceString(targetName, sizeof(targetName), "sf2_boss_override_", "", false); - SetConVarString(g_cvBossProfileOverride, targetName); - - LogSF2Message("Found sf2_boss_override entity, set boss profile override to %s", targetName); - } - } - } - - // Get a reference entity, if any. - - ent = -1; - while ((ent = FindEntityByClassname(ent, "prop_dynamic")) != -1) - { - if (g_bPageRef) break; - - GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if (targetName[0]) - { - if (StrEqual(targetName, "sf2_page_model", false)) - { - g_bPageRef = true; - GetEntPropString(ent, Prop_Data, "m_ModelName", g_strPageRefModel, sizeof(g_strPageRefModel)); - g_flPageRefModelScale = 1.0; - } - } - } - - new iPageCount = GetArraySize(hArray); - if (iPageCount) - { - SortADTArray(hArray, Sort_Random, Sort_Integer); - - decl Float:vecPos[3], Float:vecAng[3], Float:vecDir[3]; - decl page; - ent = -1; - - for (new i = 0; i < iPageCount && (i + 1) <= g_iPageMax; i++) - { - if (bool:GetArrayCell(hArray, i, 1)) - { - new Handle:hButtStallion = Handle:GetArrayCell(hArray, i); - ent = GetArrayCell(hButtStallion, GetRandomInt(0, GetArraySize(hButtStallion) - 1)); - } - else - { - ent = GetArrayCell(hArray, i); - } - - GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", vecPos); - GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", vecAng); - GetAngleVectors(vecAng, vecDir, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(vecDir, vecDir); - ScaleVector(vecDir, 1.0); - - page = CreateEntityByName("prop_dynamic_override"); - if (page != -1) - { - TeleportEntity(page, vecPos, vecAng, NULL_VECTOR); - DispatchKeyValue(page, "targetname", "sf2_page"); - - if (g_bPageRef) - { - SetEntityModel(page, g_strPageRefModel); - } - else - { - SetEntityModel(page, PAGE_MODEL); - } - - DispatchKeyValue(page, "solid", "2"); - DispatchSpawn(page); - ActivateEntity(page); - SetVariantInt(i); - AcceptEntityInput(page, "Skin"); - AcceptEntityInput(page, "EnableCollision"); - - if (g_bPageRef) - { - SetEntPropFloat(page, Prop_Send, "m_flModelScale", g_flPageRefModelScale); - } - else - { - SetEntPropFloat(page, Prop_Send, "m_flModelScale", PAGE_MODELSCALE); - } - - SDKHook(page, SDKHook_OnTakeDamage, Hook_PageOnTakeDamage); - SDKHook(page, SDKHook_SetTransmit, Hook_SlenderObjectSetTransmit); - } - } - - // Safely remove all handles. - for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) - { - if (bool:GetArrayCell(hArray, i, 1)) - { - CloseHandle(Handle:GetArrayCell(hArray, i)); - } - } - - Call_StartForward(fOnPagesSpawned); - Call_Finish(); - } - - CloseHandle(hPageTrie); - CloseHandle(hArray); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeMapEntities()"); -#endif -} - -static HandleSpecialRoundState() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleSpecialRoundState()"); -#endif - - new bool:bOld = g_bSpecialRound; - new bool:bContinuousOld = g_bSpecialRoundContinuous; - g_bSpecialRound = false; - g_bSpecialRoundNew = false; - g_bSpecialRoundContinuous = false; - - new bool:bForceNew = false; - - if (bOld) - { - if (bContinuousOld) - { - // Check if there are players who haven't played the special round yet. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedSpecialRound[i] = true; - continue; - } - - if (!g_bPlayerPlayedSpecialRound[i]) - { - // Someone didn't get to play this yet. Continue the special round. - g_bSpecialRound = true; - g_bSpecialRoundContinuous = true; - break; - } - } - } - } - - new iRoundInterval = GetConVarInt(g_cvSpecialRoundInterval); - - if (iRoundInterval > 0 && g_iSpecialRoundCount >= iRoundInterval) - { - g_bSpecialRound = true; - bForceNew = true; - } - - // Do special round force override and reset it. - if (GetConVarInt(g_cvSpecialRoundForce) >= 0) - { - g_bSpecialRound = GetConVarBool(g_cvSpecialRoundForce); - SetConVarInt(g_cvSpecialRoundForce, -1); - } - - if (g_bSpecialRound) - { - if (bForceNew || !bOld || !bContinuousOld) - { - g_bSpecialRoundNew = true; - } - - if (g_bSpecialRoundNew) - { - if (GetConVarInt(g_cvSpecialRoundBehavior) == 1) - { - g_bSpecialRoundContinuous = true; - } - else - { - // New special round, but it's not continuous. - g_bSpecialRoundContinuous = false; - } - } - } - else - { - g_bSpecialRoundContinuous = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleSpecialRoundState() -> g_bSpecialRound = %d (count = %d, new = %d, continuous = %d)", g_bSpecialRound, g_iSpecialRoundCount, g_bSpecialRoundNew, g_bSpecialRoundContinuous); -#endif -} - -bool:IsNewBossRoundRunning() -{ - return g_bNewBossRound; -} - -/** - * Returns an array which contains all the profile names valid to be chosen for a new boss round. - */ -static Handle:GetNewBossRoundProfileList() -{ - new Handle:hBossList = CloneArray(GetSelectableBossProfileList()); - - if (GetArraySize(hBossList) > 0) - { - decl String:sMainBoss[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossMain, sMainBoss, sizeof(sMainBoss)); - - new index = FindStringInArray(hBossList, sMainBoss); - if (index != -1) - { - // Main boss exists; remove him from the list. - RemoveFromArray(hBossList, index); - } - else - { - // Main boss doesn't exist; remove the first boss from the list. - RemoveFromArray(hBossList, 0); - } - } - - return hBossList; -} - -static HandleNewBossRoundState() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleNewBossRoundState()"); -#endif - - new bool:bOld = g_bNewBossRound; - new bool:bContinuousOld = g_bNewBossRoundContinuous; - g_bNewBossRound = false; - g_bNewBossRoundNew = false; - g_bNewBossRoundContinuous = false; - - new bool:bForceNew = false; - - if (bOld) - { - if (bContinuousOld) - { - // Check if there are players who haven't played the boss round yet. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedNewBossRound[i] = true; - continue; - } - - if (!g_bPlayerPlayedNewBossRound[i]) - { - // Someone didn't get to play this yet. Continue the boss round. - g_bNewBossRound = true; - g_bNewBossRoundContinuous = true; - break; - } - } - } - } - - // Don't force a new special round while a continuous round is going on. - if (!g_bNewBossRoundContinuous) - { - new iRoundInterval = GetConVarInt(g_cvNewBossRoundInterval); - - if (/*iRoundInterval > 0 &&*/ iRoundInterval <= 0 || g_iNewBossRoundCount >= iRoundInterval) - { - g_bNewBossRound = true; - bForceNew = true; - } - } - - // Do boss round force override and reset it. - if (GetConVarInt(g_cvNewBossRoundForce) >= 0) - { - g_bNewBossRound = GetConVarBool(g_cvNewBossRoundForce); - SetConVarInt(g_cvNewBossRoundForce, -1); - } - - // Check if we have enough bosses. - if (g_bNewBossRound) - { - new Handle:hBossList = GetNewBossRoundProfileList(); - - if (GetArraySize(hBossList) < 1) - { - g_bNewBossRound = false; // Not enough bosses. - } - - CloseHandle(hBossList); - } - - if (g_bNewBossRound) - { - if (bForceNew || !bOld || !bContinuousOld) - { - g_bNewBossRoundNew = true; - } - - if (g_bNewBossRoundNew) - { - if (GetConVarInt(g_cvNewBossRoundBehavior) == 1) - { - g_bNewBossRoundContinuous = true; - } - else - { - // New "new boss round", but it's not continuous. - g_bNewBossRoundContinuous = false; - } - } - } - else - { - g_bNewBossRoundContinuous = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleNewBossRoundState() -> g_bNewBossRound = %d (count = %d, new = %d, continuous = %d)", g_bNewBossRound, g_iNewBossRoundCount, g_bNewBossRoundNew, g_bNewBossRoundContinuous); -#endif -} - -/** - * Returns the amount of players that are in game and currently not eliminated. - */ -GetActivePlayerCount() -{ - new count = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - count++; - } - } - - return count; -} - -static SelectStartingBossesForRound() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START SelectStartingBossesForRound()"); -#endif - - new Handle:hSelectableBossList = GetSelectableBossProfileList(); - - // Select which boss profile to use. - decl String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); - - if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) - { - // Pick the overridden boss. - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfileOverride); - SetConVarString(g_cvBossProfileOverride, ""); - } - else if (g_bNewBossRound) - { - if (g_bNewBossRoundNew) - { - new Handle:hBossList = GetNewBossRoundProfileList(); - - GetArrayString(hBossList, GetRandomInt(0, GetArraySize(hBossList) - 1), g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile)); - - CloseHandle(hBossList); - } - - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), g_strNewBossRoundProfile); - } - else - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetConVarString(g_cvBossMain, sProfile, sizeof(sProfile)); - - if (strlen(sProfile) > 0 && IsProfileValid(sProfile)) - { - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfile); - } - else - { - if (GetArraySize(hSelectableBossList) > 0) - { - // Pick the first boss in our array if the main boss doesn't exist. - GetArrayString(hSelectableBossList, 0, g_strRoundBossProfile, sizeof(g_strRoundBossProfile)); - } - else - { - // No bosses to pick. What? - strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), ""); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END SelectStartingBossesForRound() -> boss: %s", g_strRoundBossProfile); -#endif -} - -static GetRoundIntroParameters() -{ - g_iRoundIntroFadeColor[0] = 0; - g_iRoundIntroFadeColor[1] = 0; - g_iRoundIntroFadeColor[2] = 0; - g_iRoundIntroFadeColor[3] = 255; - - g_flRoundIntroFadeHoldTime = GetConVarFloat(g_cvIntroDefaultHoldTime); - g_flRoundIntroFadeDuration = GetConVarFloat(g_cvIntroDefaultFadeTime); - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "env_fade")) != -1) - { - decl String:sName[32]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_intro_fade", false)) - { - new iColorOffset = FindSendPropOffs("CBaseEntity", "m_clrRender"); - if (iColorOffset != -1) - { - g_iRoundIntroFadeColor[0] = GetEntData(ent, iColorOffset, 1); - g_iRoundIntroFadeColor[1] = GetEntData(ent, iColorOffset + 1, 1); - g_iRoundIntroFadeColor[2] = GetEntData(ent, iColorOffset + 2, 1); - g_iRoundIntroFadeColor[3] = GetEntData(ent, iColorOffset + 3, 1); - } - - g_flRoundIntroFadeHoldTime = GetEntPropFloat(ent, Prop_Data, "m_HoldTime"); - g_flRoundIntroFadeDuration = GetEntPropFloat(ent, Prop_Data, "m_Duration"); - - break; - } - } - - // Get the intro music. - strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), SF2_INTRO_DEFAULT_MUSIC); - - ent = -1; - while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) - { - decl String:sName[64]; - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - - if (StrEqual(sName, "sf2_intro_music", false)) - { - decl String:sSongPath[PLATFORM_MAX_PATH]; - GetEntPropString(ent, Prop_Data, "m_iszSound", sSongPath, sizeof(sSongPath)); - - if (strlen(sSongPath) == 0) - { - LogError("Found sf2_intro_music entity, but it has no sound path specified! Default intro music will be used instead."); - } - else - { - strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), sSongPath); - } - - break; - } - } -} - -static GetRoundEscapeParameters() -{ - g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; - - decl String:sName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (!StrContains(sName, "sf2_escape_spawnpoint", false)) - { - g_iRoundEscapePointEntity = EntIndexToEntRef(ent); - break; - } - } -} - -InitializeNewGame() -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeNewGame()"); -#endif - - GetRoundIntroParameters(); - GetRoundEscapeParameters(); - - // Choose round state. - if (GetConVarBool(g_cvIntroEnabled)) - { - // Set the round state to the intro stage. - SetRoundState(SF2RoundState_Intro); - } - else - { - SetRoundState(SF2RoundState_Active); - } - - if (g_iRoundActiveCount == 1) - { - SetConVarString(g_cvBossProfileOverride, ""); - } - - HandleSpecialRoundState(); - - // Was a new special round initialized? - if (g_bSpecialRound) - { - if (g_bSpecialRoundNew) - { - // Reset round count. - g_iSpecialRoundCount = 1; - - if (g_bSpecialRoundContinuous) - { - // It's the start of a continuous special round. - - // Initialize all players' values. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedSpecialRound[i] = true; - continue; - } - - g_bPlayerPlayedSpecialRound[i] = false; - } - } - - SpecialRoundCycleStart(); - } - else - { - SpecialRoundStart(); - - if (g_bSpecialRoundContinuous) - { - // Display the current special round going on to late players. - CreateTimer(3.0, Timer_DisplaySpecialRound, _, TIMER_FLAG_NO_MAPCHANGE); - } - } - } - else - { - g_iSpecialRoundCount++; - - SpecialRoundReset(); - } - - // Determine boss round state. - HandleNewBossRoundState(); - - if (g_bNewBossRound) - { - if (g_bNewBossRoundNew) - { - // Reset round count; - g_iNewBossRoundCount = 1; - - if (g_bNewBossRoundContinuous) - { - // It's the start of a continuous "new boss round". - - // Initialize all players' values. - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || !IsClientParticipating(i)) - { - g_bPlayerPlayedNewBossRound[i] = true; - continue; - } - - g_bPlayerPlayedNewBossRound[i] = false; - } - } - } - } - else - { - g_iNewBossRoundCount++; - } - - InitializeMapEntities(); - - // Initialize pages and entities. - GetPageMusicRanges(); - - SelectStartingBossesForRound(); - - ForceInNextPlayersInQueue(GetMaxPlayersForRound()); - - // Respawn all players, if needed. - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientParticipating(i)) - { - if (!HandlePlayerTeam(i)) - { - if (!g_bPlayerEliminated[i]) - { - // Players currently in the "game" still have to be respawned. - TF2_RespawnPlayer(i); - } - } - } - } - - if (GetRoundState() == SF2RoundState_Intro) - { - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (!g_bPlayerEliminated[i]) - { - if (!IsFakeClient(i)) - { - // Currently in intro state, play intro music. - g_hPlayerIntroMusicTimer[i] = CreateTimer(0.5, Timer_PlayIntroMusicToPlayer, GetClientUserId(i), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; - } - } - else - { - g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; - } - } - } - else - { - // Spawn the boss! - SelectProfile(0, g_strRoundBossProfile); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeNewGame()"); -#endif -} - -public Action:Timer_PlayIntroMusicToPlayer(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerIntroMusicTimer[client]) return; - - g_hPlayerIntroMusicTimer[client] = INVALID_HANDLE; - - EmitSoundToClient(client, g_strRoundIntroMusic, _, MUSIC_CHAN, SNDLEVEL_NONE); -} - -public Action:Timer_IntroTextSequence(Handle:timer) -{ - if (!g_bEnabled) return; - if (g_hRoundIntroTextTimer != timer) return; - - new Float:flDuration = 0.0; - - if (g_iRoundIntroText != 0) - { - new bool:bFoundGameText = false; - - new iClients[MAXPLAYERS + 1]; - new iClientsNum; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; - - iClients[iClientsNum] = i; - iClientsNum++; - } - - if (!g_bRoundIntroTextDefault) - { - decl String:sTargetname[64]; - Format(sTargetname, sizeof(sTargetname), "sf2_intro_text_%d", g_iRoundIntroText); - - new iGameText = FindEntityByTargetname(sTargetname, "game_text"); - if (iGameText && iGameText != INVALID_ENT_REFERENCE) - { - bFoundGameText = true; - flDuration = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); - - decl String:sMessage[512]; - GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); - ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); - } - } - else - { - if (g_iRoundIntroText == 2) - { - bFoundGameText = false; - - decl String:sMessage[64]; - GetCurrentMap(sMessage, sizeof(sMessage)); - - for (new i = 0; i < iClientsNum; i++) - { - ClientShowMainMessage(iClients[i], sMessage, 1); - } - } - } - - if (g_iRoundIntroText == 1 && !bFoundGameText) - { - // Use default intro sequence. Eugh. - g_bRoundIntroTextDefault = true; - flDuration = GetConVarFloat(g_cvIntroDefaultHoldTime) / 2.0; - - for (new i = 0; i < iClientsNum; i++) - { - EmitSoundToClient(iClients[i], SF2_INTRO_DEFAULT_MUSIC, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - else - { - if (!bFoundGameText) return; // done with sequence; don't check anymore. - } - } - - g_iRoundIntroText++; - g_hRoundIntroTextTimer = CreateTimer(flDuration, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ActivateRoundFromIntro(Handle:timer) -{ - if (!g_bEnabled) return; - if (g_hRoundIntroTimer != timer) return; - - // Obviously we don't want to spawn the boss when g_strRoundBossProfile isn't set yet. - SetRoundState(SF2RoundState_Active); - - // Spawn the boss! - SelectProfile(0, g_strRoundBossProfile); -} - -CheckRoundWinConditions() -{ - if (IsRoundInWarmup() || IsRoundEnding()) return; - - new iTotalCount = 0; - new iAliveCount = 0; - new iEscapedCount = 0; - - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - iTotalCount++; - if (!g_bPlayerEliminated[i] && !IsClientInDeathCam(i)) - { - iAliveCount++; - if (DidClientEscape(i)) iEscapedCount++; - } - } - - if (iAliveCount == 0) - { - ForceTeamWin(_:TFTeam_Blue); - } - else - { - if (g_bRoundHasEscapeObjective) - { - if (iEscapedCount == iAliveCount) - { - ForceTeamWin(_:TFTeam_Red); - } - } - else - { - if (g_iPageMax > 0 && g_iPageCount == g_iPageMax) - { - ForceTeamWin(_:TFTeam_Red); - } - } - } -} - -// ========================================================== -// API -// ========================================================== - -public Native_IsRunning(Handle:plugin, numParams) -{ - return g_bEnabled; -} - -public Native_GetCurrentDifficulty(Handle:plugin, numParams) -{ - return GetConVarInt(g_cvDifficulty); -} - -public Native_GetDifficultyModifier(Handle:plugin, numParams) -{ - new iDifficulty = GetNativeCell(1); - if (iDifficulty < Difficulty_Easy || iDifficulty >= Difficulty_Max) - { - LogError("Difficulty parameter can only be from %d to %d!", Difficulty_Easy, Difficulty_Max - 1); - return _:1.0; - } - - switch (iDifficulty) - { - case Difficulty_Easy: return _:DIFFICULTY_EASY; - case Difficulty_Hard: return _:DIFFICULTY_HARD; - case Difficulty_Insane: return _:DIFFICULTY_INSANE; - } - - return _:DIFFICULTY_NORMAL; -} - -public Native_IsClientEliminated(Handle:plugin, numParams) -{ - return g_bPlayerEliminated[GetNativeCell(1)]; -} - -public Native_IsClientInGhostMode(Handle:plugin, numParams) -{ - return IsClientInGhostMode(GetNativeCell(1)); -} - -public Native_IsClientProxy(Handle:plugin, numParams) -{ - return g_bPlayerProxy[GetNativeCell(1)]; -} - -public Native_GetClientBlinkCount(Handle:plugin, numParams) -{ - return ClientGetBlinkCount(GetNativeCell(1)); -} - -public Native_GetClientProxyMaster(Handle:plugin, numParams) -{ - return NPCGetFromUniqueID(g_iPlayerProxyMaster[GetNativeCell(1)]); -} - -public Native_GetClientProxyControlAmount(Handle:plugin, numParams) -{ - return g_iPlayerProxyControl[GetNativeCell(1)]; -} - -public Native_GetClientProxyControlRate(Handle:plugin, numParams) -{ - return _:g_flPlayerProxyControlRate[GetNativeCell(1)]; -} - -public Native_SetClientProxyMaster(Handle:plugin, numParams) -{ - g_iPlayerProxyMaster[GetNativeCell(1)] = NPCGetUniqueID(GetNativeCell(2)); -} - -public Native_SetClientProxyControlAmount(Handle:plugin, numParams) -{ - g_iPlayerProxyControl[GetNativeCell(1)] = GetNativeCell(2); -} - -public Native_SetClientProxyControlRate(Handle:plugin, numParams) -{ - g_flPlayerProxyControlRate[GetNativeCell(1)] = Float:GetNativeCell(2); -} - -public Native_IsClientLookingAtBoss(Handle:plugin, numParams) -{ - return g_bPlayerSeesSlender[GetNativeCell(1)][GetNativeCell(2)]; -} - -public Native_CollectAsPage(Handle:plugin, numParams) -{ - CollectPage(GetNativeCell(1), GetNativeCell(2)); -} - -public Native_GetMaxBosses(Handle:plugin, numParams) -{ - return MAX_BOSSES; -} - -public Native_EntIndexToBossIndex(Handle:plugin, numParams) -{ - return NPCGetFromEntIndex(GetNativeCell(1)); -} - -public Native_BossIndexToEntIndex(Handle:plugin, numParams) -{ - return NPCGetEntIndex(GetNativeCell(1)); -} - -public Native_BossIDToBossIndex(Handle:plugin, numParams) -{ - return NPCGetFromUniqueID(GetNativeCell(1)); -} - -public Native_BossIndexToBossID(Handle:plugin, numParams) -{ - return NPCGetUniqueID(GetNativeCell(1)); -} - -public Native_GetBossName(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(GetNativeCell(1), sProfile, sizeof(sProfile)); - - SetNativeString(2, sProfile, GetNativeCell(3)); -} - -public Native_GetBossModelEntity(Handle:plugin, numParams) -{ - return EntRefToEntIndex(g_iSlenderModel[GetNativeCell(1)]); -} - -public Native_GetBossTarget(Handle:plugin, numParams) -{ - return EntRefToEntIndex(g_iSlenderTarget[GetNativeCell(1)]); -} - -public Native_GetBossMaster(Handle:plugin, numParams) -{ - return g_iSlenderCopyMaster[GetNativeCell(1)]; -} - -public Native_GetBossState(Handle:plugin, numParams) -{ - return g_iSlenderState[GetNativeCell(1)]; -} - -public Native_IsBossProfileValid(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - return IsProfileValid(sProfile); -} - -public Native_GetBossProfileNum(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - return GetProfileNum(sProfile, sKeyValue, GetNativeCell(3)); -} - -public Native_GetBossProfileFloat(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - return _:GetProfileFloat(sProfile, sKeyValue, Float:GetNativeCell(3)); -} - -public Native_GetBossProfileString(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - new iResultLen = GetNativeCell(4); - decl String:sResult[iResultLen]; - - decl String:sDefaultValue[512]; - GetNativeString(5, sDefaultValue, sizeof(sDefaultValue)); - - new bool:bSuccess = GetProfileString(sProfile, sKeyValue, sResult, iResultLen, sDefaultValue); - - SetNativeString(3, sResult, iResultLen); - return bSuccess; -} - -public Native_GetBossProfileVector(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - decl Float:flResult[3]; - decl Float:flDefaultValue[3]; - GetNativeArray(4, flDefaultValue, 3); - - new bool:bSuccess = GetProfileVector(sProfile, sKeyValue, flResult, flDefaultValue); - - SetNativeArray(3, flResult, 3); - return bSuccess; -} - -public Native_GetRandomStringFromBossProfile(Handle:plugin, numParams) -{ - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); - - decl String:sKeyValue[256]; - GetNativeString(2, sKeyValue, sizeof(sKeyValue)); - - new iBufferLen = GetNativeCell(4); - decl String:sBuffer[iBufferLen]; - - new iIndex = GetNativeCell(5); - - new bool:bSuccess = GetRandomStringFromProfile(sProfile, sKeyValue, sBuffer, iBufferLen, iIndex); - SetNativeString(3, sBuffer, iBufferLen); - return bSuccess; +#include <sourcemod> +#include <sdktools> +#include <sdkhooks> +#include <clientprefs> +#include <steamtools> +#include <tf2items> +#include <dhooks> +#include <navmesh> + +#include <tf2> +#include <tf2_stocks> +#include <morecolors> +#include <sf2> + +#undef REQUIRE_PLUGIN +#include <adminmenu> +#tryinclude <store/store-tf2footprints> +#define REQUIRE_PLUGIN + +// #define DEBUG + +// If compiling with SM 1.7+, uncomment to compile and use SF2 methodmaps. +//#define METHODMAPS + +#define PLUGIN_VERSION "0.2.6-git136" +#define PLUGIN_VERSION_DISPLAY "0.2.6" + +public Plugin:myinfo = +{ + name = "RYTP Horror (Slender Fortress edit by lexuzieel special for Penek-Gaming.Ru)", + author = "KitRifty", + description = "Based on the game Slender: The Eight Pages.", + version = PLUGIN_VERSION, + url = "http://steamcommunity.com/groups/SlenderFortress" +} + +#define FILE_RESTRICTEDWEAPONS "configs/sf2/restrictedweapons.cfg" + +#define BOSS_THINKRATE 0.1 // doesn't really matter much since timers go at a minimum of 0.1 seconds anyways + +#define CRIT_SOUND "player/crit_hit.wav" +#define CRIT_PARTICLENAME "crit_text" + +#define PAGE_MODEL "models/rytp/horror/props/hint_paper.mdl" +#define PAGE_MODELSCALE 1.1 + +#define FLASHLIGHT_CLICKSOUND "rytp_horror/toggleflashlight.wav" +#define FLASHLIGHT_BREAKSOUND "ambient/energy/spark6.wav" +#define FLASHLIGHT_NOSOUND "player/suit_denydevice.wav" +#define PAGE_GRABSOUND "rytp_horror/grabpage_sound.wav" + +#define MUSIC_CHAN SNDCHAN_AUTO + +#define MUSIC_GOTPAGES1_SOUND "rytp_horror/grabpage_music_1.wav" +#define MUSIC_GOTPAGES2_SOUND "rytp_horror/grabpage_music_2.wav" +#define MUSIC_GOTPAGES3_SOUND "rytp_horror/grabpage_music_3.wav" +#define MUSIC_GOTPAGES4_SOUND "rytp_horror/grabpage_music_4.wav" +#define MUSIC_PAGE_VOLUME 1.0 + +#define SF2_INTRO_DEFAULT_MUSIC "rytp_horror/intro_music.mp3" + +#define SF2_HUD_TEXT_COLOR_R 127 +#define SF2_HUD_TEXT_COLOR_G 167 +#define SF2_HUD_TEXT_COLOR_B 141 +#define SF2_HUD_TEXT_COLOR_A 255 + +enum MuteMode +{ + MuteMode_Normal = 0, + MuteMode_DontHearOtherTeam, + MuteMode_DontHearOtherTeamIfNotProxy +}; + +// Offsets. +new g_offsPlayerFOV = -1; +new g_offsPlayerDefaultFOV = -1; +new g_offsPlayerFogCtrl = -1; +new g_offsPlayerPunchAngle = -1; +new g_offsPlayerPunchAngleVel = -1; +new g_offsFogCtrlEnable = -1; +new g_offsFogCtrlEnd = -1; + +new g_iParticleCriticalHit = -1; + +new bool:g_bEnabled; + +new Handle:g_hConfig; +new Handle:g_hRestrictedWeaponsConfig; +new Handle:g_hSpecialRoundsConfig; + +new Handle:g_hPageMusicRanges; + +new g_iSlenderModel[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new g_iSlenderPoseEnt[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new g_iSlenderCopyMaster[MAX_BOSSES] = { -1, ... }; +new Float:g_flSlenderEyePosOffset[MAX_BOSSES][3]; +new Float:g_flSlenderEyeAngOffset[MAX_BOSSES][3]; +new Float:g_flSlenderDetectMins[MAX_BOSSES][3]; +new Float:g_flSlenderDetectMaxs[MAX_BOSSES][3]; +new Handle:g_hSlenderThink[MAX_BOSSES]; +new Handle:g_hSlenderEntityThink[MAX_BOSSES]; +new Handle:g_hSlenderFakeTimer[MAX_BOSSES]; +new Float:g_flSlenderLastKill[MAX_BOSSES]; +new g_iSlenderState[MAX_BOSSES]; +new g_iSlenderTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; +new Float:g_flSlenderAcceleration[MAX_BOSSES]; +new Float:g_flSlenderGoalPos[MAX_BOSSES][3]; +new Float:g_flSlenderStaticRadius[MAX_BOSSES]; +new Float:g_flSlenderChaseDeathPosition[MAX_BOSSES][3]; +new bool:g_bSlenderChaseDeathPosition[MAX_BOSSES]; +new Float:g_flSlenderIdleAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderWalkAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderRunAnimationPlaybackRate[MAX_BOSSES]; +new Float:g_flSlenderJumpSpeed[MAX_BOSSES]; +new Float:g_flSlenderPathNodeTolerance[MAX_BOSSES]; +new Float:g_flSlenderPathNodeLookAhead[MAX_BOSSES]; +new bool:g_bSlenderFeelerReflexAdjustment[MAX_BOSSES]; +new Float:g_flSlenderFeelerReflexAdjustmentPos[MAX_BOSSES][3]; + +new g_iSlenderTeleportTarget[MAX_BOSSES] = { INVALID_ENT_REFERENCE, ... }; + +new Float:g_flSlenderNextTeleportTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportTargetTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMinRange[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxRange[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxTargetTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTeleportMaxTargetStress[MAX_BOSSES] = { 0.0, ... }; +new Float:g_flSlenderTeleportPlayersRestTime[MAX_BOSSES][MAXPLAYERS + 1]; + +// For boss type 2 +// General variables +new g_iSlenderHealth[MAX_BOSSES]; +new Handle:g_hSlenderPath[MAX_BOSSES]; +new g_iSlenderCurrentPathNode[MAX_BOSSES] = { -1, ... }; +new bool:g_bSlenderAttacking[MAX_BOSSES]; +new Handle:g_hSlenderAttackTimer[MAX_BOSSES]; +new Float:g_flSlenderNextJump[MAX_BOSSES] = { -1.0, ... }; +new g_iSlenderInterruptConditions[MAX_BOSSES]; +new Float:g_flSlenderLastFoundPlayer[MAX_BOSSES][MAXPLAYERS + 1]; +new Float:g_flSlenderLastFoundPlayerPos[MAX_BOSSES][MAXPLAYERS + 1][3]; +new Float:g_flSlenderNextPathTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderCalculatedWalkSpeed[MAX_BOSSES]; +new Float:g_flSlenderCalculatedSpeed[MAX_BOSSES]; +new Float:g_flSlenderTimeUntilNoPersistence[MAX_BOSSES]; + +new Float:g_flSlenderProxyTeleportMinRange[MAX_BOSSES]; +new Float:g_flSlenderProxyTeleportMaxRange[MAX_BOSSES]; + +// Sound variables +new Float:g_flSlenderTargetSoundLastTime[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTargetSoundMasterPos[MAX_BOSSES][3]; // to determine hearing focus +new Float:g_flSlenderTargetSoundTempPos[MAX_BOSSES][3]; +new Float:g_flSlenderTargetSoundDiscardMasterPosTime[MAX_BOSSES]; +new bool:g_bSlenderInvestigatingSound[MAX_BOSSES]; +new SoundType:g_iSlenderTargetSoundType[MAX_BOSSES] = { SoundType_None, ... }; +new g_iSlenderTargetSoundCount[MAX_BOSSES]; +new Float:g_flSlenderLastHeardVoice[MAX_BOSSES]; +new Float:g_flSlenderLastHeardFootstep[MAX_BOSSES]; +new Float:g_flSlenderLastHeardWeapon[MAX_BOSSES]; + + +new Float:g_flSlenderNextJumpScare[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextVoiceSound[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextMoanSound[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderNextWanderPos[MAX_BOSSES] = { -1.0, ... }; + + +new Float:g_flSlenderTimeUntilRecover[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilAlert[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilIdle[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilChase[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilKill[MAX_BOSSES] = { -1.0, ... }; +new Float:g_flSlenderTimeUntilNextProxy[MAX_BOSSES] = { -1.0, ... }; + +// Page data. +new g_iPageCount; +new g_iPageMax; +new Float:g_flPageFoundLastTime; +new bool:g_bPageRef; +new String:g_strPageRefModel[PLATFORM_MAX_PATH]; +new Float:g_flPageRefModelScale; + +static Handle:g_hPlayerIntroMusicTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Seeing Mr. Slendy data. +new bool:g_bPlayerSeesSlender[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerSeesSlenderLastTime[MAXPLAYERS + 1][MAX_BOSSES]; + +new Float:g_flPlayerSightSoundNextTime[MAXPLAYERS + 1][MAX_BOSSES]; + +new Float:g_flPlayerScareLastTime[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerScareNextTime[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerStaticAmount[MAXPLAYERS + 1]; + +new Float:g_flPlayerLastChaseBossEncounterTime[MAXPLAYERS + 1][MAX_BOSSES]; + +// Player static data. +new g_iPlayerStaticMode[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerStaticIncreaseRate[MAXPLAYERS + 1]; +new Float:g_flPlayerStaticDecreaseRate[MAXPLAYERS + 1]; +new Handle:g_hPlayerStaticTimer[MAXPLAYERS + 1]; +new g_iPlayerStaticMaster[MAXPLAYERS + 1] = { -1, ... }; +new String:g_strPlayerStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new String:g_strPlayerLastStaticSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerLastStaticTime[MAXPLAYERS + 1]; +new Float:g_flPlayerLastStaticVolume[MAXPLAYERS + 1]; +new Handle:g_hPlayerLastStaticTimer[MAXPLAYERS + 1]; + +// Static shake data. +new g_iPlayerStaticShakeMaster[MAXPLAYERS + 1]; +new bool:g_bPlayerInStaticShake[MAXPLAYERS + 1]; +new String:g_strPlayerStaticShakeSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerStaticShakeMinVolume[MAXPLAYERS + 1]; +new Float:g_flPlayerStaticShakeMaxVolume[MAXPLAYERS + 1]; + +// Fake lag compensation for FF. +new bool:g_bPlayerLagCompensation[MAXPLAYERS + 1]; +new g_iPlayerLagCompensationTeam[MAXPLAYERS + 1]; + +// Hint data. +enum +{ + PlayerHint_Sprint = 0, + PlayerHint_Flashlight, + PlayerHint_MainMenu, + PlayerHint_Blink, + PlayerHint_MaxNum +}; + +enum PlayerPreferences +{ + bool:PlayerPreference_PvPAutoSpawn, + MuteMode:PlayerPreference_MuteMode, + bool:PlayerPreference_FilmGrain, + bool:PlayerPreference_ShowHints, + bool:PlayerPreference_EnableProxySelection, + bool:PlayerPreference_ProjectedFlashlight, + bool:PlayerPreference_GhostOverlay +}; + +new bool:g_bPlayerHints[MAXPLAYERS + 1][PlayerHint_MaxNum]; +new g_iPlayerPreferences[MAXPLAYERS + 1][PlayerPreferences]; + +// Player data. +new g_iPlayerLastButtons[MAXPLAYERS + 1]; +new bool:g_bPlayerChoseTeam[MAXPLAYERS + 1]; +new bool:g_bPlayerEliminated[MAXPLAYERS + 1]; +new bool:g_bPlayerEscaped[MAXPLAYERS + 1]; +new g_iPlayerPageCount[MAXPLAYERS + 1]; +new g_iPlayerQueuePoints[MAXPLAYERS + 1]; +new bool:g_bPlayerPlaying[MAXPLAYERS + 1]; +new Handle:g_hPlayerOverlayCheck[MAXPLAYERS + 1]; + +new Handle:g_hPlayerSwitchBlueTimer[MAXPLAYERS + 1]; + +// Player stress data. +new Float:g_flPlayerStress[MAXPLAYERS + 1]; +new Float:g_flPlayerStressNextUpdateTime[MAXPLAYERS + 1]; + +// Proxy data. +new bool:g_bPlayerProxy[MAXPLAYERS + 1]; +new bool:g_bPlayerProxyAvailable[MAXPLAYERS + 1]; +new Handle:g_hPlayerProxyAvailableTimer[MAXPLAYERS + 1]; +new bool:g_bPlayerProxyAvailableInForce[MAXPLAYERS + 1]; +new g_iPlayerProxyAvailableCount[MAXPLAYERS + 1]; +new g_iPlayerProxyMaster[MAXPLAYERS + 1]; +new g_iPlayerProxyControl[MAXPLAYERS + 1]; +new Handle:g_hPlayerProxyControlTimer[MAXPLAYERS + 1]; +new Float:g_flPlayerProxyControlRate[MAXPLAYERS + 1]; +new Handle:g_flPlayerProxyVoiceTimer[MAXPLAYERS + 1]; +new g_iPlayerProxyAskMaster[MAXPLAYERS + 1] = { -1, ... }; +new Float:g_iPlayerProxyAskPosition[MAXPLAYERS + 1][3]; + +new g_iPlayerDesiredFOV[MAXPLAYERS + 1]; + +new Handle:g_hPlayerPostWeaponsTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Music system. +new g_iPlayerMusicFlags[MAXPLAYERS + 1]; +new String:g_strPlayerMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerMusicVolume[MAXPLAYERS + 1]; +new Float:g_flPlayerMusicTargetVolume[MAXPLAYERS + 1]; +new Handle:g_hPlayerMusicTimer[MAXPLAYERS + 1]; +new g_iPlayerPageMusicMaster[MAXPLAYERS + 1]; + +// Chase music system, which apparently also uses the alert song system. And the idle sound system. +new String:g_strPlayerChaseMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new String:g_strPlayerChaseMusicSee[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerChaseMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Float:g_flPlayerChaseMusicSeeVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerChaseMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerChaseMusicSeeTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayerChaseMusicMaster[MAXPLAYERS + 1] = { -1, ... }; +new g_iPlayerChaseMusicSeeMaster[MAXPLAYERS + 1] = { -1, ... }; + +new String:g_strPlayerAlertMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayerAlertMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayerAlertMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayerAlertMusicMaster[MAXPLAYERS + 1] = { -1, ... }; + +new String:g_strPlayer20DollarsMusic[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; +new Float:g_flPlayer20DollarsMusicVolumes[MAXPLAYERS + 1][MAX_BOSSES]; +new Handle:g_hPlayer20DollarsMusicTimer[MAXPLAYERS + 1][MAX_BOSSES]; +new g_iPlayer20DollarsMusicMaster[MAXPLAYERS + 1] = { -1, ... }; + +// Player overlay data +new Handle:g_hOverlayUpdateTimer[MAXPLAYERS + 1]; + +new SF2RoundState:g_iRoundState = SF2RoundState_Invalid; +new bool:g_bRoundGrace = false; +new Float:g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; +new bool:g_bRoundInfiniteFlashlight = false; +new bool:g_bRoundInfiniteBlink = false; +new bool:g_bRoundInfiniteSprint = false; + +static Handle:g_hRoundGraceTimer = INVALID_HANDLE; +static Handle:g_hRoundTimer = INVALID_HANDLE; +static Handle:g_hVoteTimer = INVALID_HANDLE; +static String:g_strRoundBossProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + +static g_iRoundCount = 0; +static g_iRoundEndCount = 0; +static g_iRoundActiveCount = 0; +static g_iRoundTime = 0; +static g_iRoundTimeLimit = 0; +static g_iRoundEscapeTimeLimit = 0; +static g_iRoundTimeGainFromPage = 0; +static bool:g_bRoundHasEscapeObjective = false; + +static g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; + +static g_iRoundIntroFadeColor[4] = { 255, ... }; +static Float:g_flRoundIntroFadeHoldTime; +static Float:g_flRoundIntroFadeDuration; +static Handle:g_hRoundIntroTimer = INVALID_HANDLE; +static bool:g_bRoundIntroTextDefault = true; +static Handle:g_hRoundIntroTextTimer = INVALID_HANDLE; +static g_iRoundIntroText; +static String:g_strRoundIntroMusic[PLATFORM_MAX_PATH] = ""; + +static g_iRoundWarmupRoundCount = 0; + +static bool:g_bRoundWaitingForPlayers = false; + +// Special round variables. +new bool:g_bSpecialRound = false; +new g_iSpecialRoundType = 0; +new bool:g_bSpecialRoundNew = false; +new bool:g_bSpecialRoundContinuous = false; +new g_iSpecialRoundCount = 1; +new bool:g_bPlayerPlayedSpecialRound[MAXPLAYERS + 1] = { true, ... }; + +// New boss round variables. +static bool:g_bNewBossRound = false; +static bool:g_bNewBossRoundNew = false; +static bool:g_bNewBossRoundContinuous = false; +static g_iNewBossRoundCount = 1; +static bool:g_bPlayerPlayedNewBossRound[MAXPLAYERS + 1] = { true, ... }; +static String:g_strNewBossRoundProfile[64] = ""; + +static Handle:g_hRoundMessagesTimer = INVALID_HANDLE; +static g_iRoundMessagesNum = 0; + +static Handle:g_hBossCountUpdateTimer = INVALID_HANDLE; +static Handle:g_hClientAverageUpdateTimer = INVALID_HANDLE; + +// Server variables. +new Handle:g_cvVersion; +new Handle:g_cvEnabled; +new Handle:g_cvSlenderMapsOnly; +new Handle:g_cvPlayerViewbobEnabled; +new Handle:g_cvPlayerShakeEnabled; +new Handle:g_cvPlayerShakeFrequencyMax; +new Handle:g_cvPlayerShakeAmplitudeMax; +new Handle:g_cvGraceTime; +new Handle:g_cvAllChat; +new Handle:g_cv20Dollars; +new Handle:g_cvMaxPlayers; +new Handle:g_cvMaxPlayersOverride; +new Handle:g_cvCampingEnabled; +new Handle:g_cvCampingMaxStrikes; +new Handle:g_cvCampingStrikesWarn; +new Handle:g_cvCampingMinDistance; +new Handle:g_cvCampingNoStrikeSanity; +new Handle:g_cvCampingNoStrikeBossDistance; +new Handle:g_cvDifficulty; +new Handle:g_cvBossMain; +new Handle:g_cvBossProfileOverride; +new Handle:g_cvPlayerBlinkRate; +new Handle:g_cvPlayerBlinkHoldTime; +new Handle:g_cvSpecialRoundBehavior; +new Handle:g_cvSpecialRoundForce; +new Handle:g_cvSpecialRoundOverride; +new Handle:g_cvSpecialRoundInterval; +new Handle:g_cvNewBossRoundBehavior; +new Handle:g_cvNewBossRoundInterval; +new Handle:g_cvNewBossRoundForce; +new Handle:g_cvPlayerVoiceDistance; +new Handle:g_cvPlayerVoiceWallScale; +new Handle:g_cvUltravisionEnabled; +new Handle:g_cvUltravisionRadiusRed; +new Handle:g_cvUltravisionRadiusBlue; +new Handle:g_cvUltravisionBrightness; +new Handle:g_cvGhostModeConnectionCheck; +new Handle:g_cvGhostModeConnectionTolerance; +new Handle:g_cvIntroEnabled; +new Handle:g_cvIntroDefaultHoldTime; +new Handle:g_cvIntroDefaultFadeTime; +new Handle:g_cvTimeLimit; +new Handle:g_cvTimeLimitEscape; +new Handle:g_cvTimeGainFromPageGrab; +new Handle:g_cvWarmupRound; +new Handle:g_cvWarmupRoundNum; +new Handle:g_cvPlayerViewbobHurtEnabled; +new Handle:g_cvPlayerViewbobSprintEnabled; +new Handle:g_cvPlayerFakeLagCompensation; +new Handle:g_cvPlayerProxyWaitTime; +new Handle:g_cvPlayerProxyAsk; +new Handle:g_cvHalfZatoichiHealthGain; +new Handle:g_cvBlockSuicideDuringRound; + +new Handle:g_cvPlayerInfiniteSprintOverride; +new Handle:g_cvPlayerInfiniteFlashlightOverride; +new Handle:g_cvPlayerInfiniteBlinkOverride; + +new Handle:g_cvGravity; +new Float:g_flGravity; + +new Handle:g_cvMaxRounds; + +new bool:g_b20Dollars; + +new bool:g_bPlayerShakeEnabled; +new bool:g_bPlayerViewbobEnabled; +new bool:g_bPlayerViewbobHurtEnabled; +new bool:g_bPlayerViewbobSprintEnabled; + +new Handle:g_hHudSync; +new Handle:g_hHudSync2; +new Handle:g_hRoundTimerSync; + +new Handle:g_hCookie; + +// Global forwards. +new Handle:fOnBossAdded; +new Handle:fOnBossSpawn; +new Handle:fOnBossChangeState; +new Handle:fOnBossRemoved; +new Handle:fOnPagesSpawned; +new Handle:fOnClientBlink; +new Handle:fOnClientCaughtByBoss; +new Handle:fOnClientGiveQueuePoints; +new Handle:fOnClientActivateFlashlight; +new Handle:fOnClientDeactivateFlashlight; +new Handle:fOnClientBreakFlashlight; +new Handle:fOnClientEscape; +new Handle:fOnClientLooksAtBoss; +new Handle:fOnClientLooksAwayFromBoss; +new Handle:fOnClientStartDeathCam; +new Handle:fOnClientEndDeathCam; +new Handle:fOnClientGetDefaultWalkSpeed; +new Handle:fOnClientGetDefaultSprintSpeed; +new Handle:fOnClientSpawnedAsProxy; +new Handle:fOnClientDamagedByBoss; +new Handle:fOnGroupGiveQueuePoints; + +new Handle:g_hSDKWeaponScattergun; +new Handle:g_hSDKWeaponPistolScout; +new Handle:g_hSDKWeaponBat; +new Handle:g_hSDKWeaponSniperRifle; +new Handle:g_hSDKWeaponSMG; +new Handle:g_hSDKWeaponKukri; +new Handle:g_hSDKWeaponRocketLauncher; +new Handle:g_hSDKWeaponShotgunSoldier; +new Handle:g_hSDKWeaponShovel; +new Handle:g_hSDKWeaponGrenadeLauncher; +new Handle:g_hSDKWeaponStickyLauncher; +new Handle:g_hSDKWeaponBottle; +new Handle:g_hSDKWeaponMinigun; +new Handle:g_hSDKWeaponShotgunHeavy; +new Handle:g_hSDKWeaponFists; +new Handle:g_hSDKWeaponSyringeGun; +new Handle:g_hSDKWeaponMedigun; +new Handle:g_hSDKWeaponBonesaw; +new Handle:g_hSDKWeaponFlamethrower; +new Handle:g_hSDKWeaponShotgunPyro; +new Handle:g_hSDKWeaponFireaxe; +new Handle:g_hSDKWeaponRevolver; +new Handle:g_hSDKWeaponKnife; +new Handle:g_hSDKWeaponInvis; +new Handle:g_hSDKWeaponShotgunPrimary; +new Handle:g_hSDKWeaponPistol; +new Handle:g_hSDKWeaponWrench; + +new Handle:g_hSDKGetMaxHealth; +new Handle:g_hSDKWantsLagCompensationOnEntity; +new Handle:g_hSDKShouldTransmit; + +#include "rytp_horror/stocks.sp" +#include "rytp_horror/overlay.sp" +#include "rytp_horror/logging.sp" +#include "rytp_horror/debug.sp" +#include "rytp_horror/profiles.sp" +#include "rytp_horror/nav.sp" +#include "rytp_horror/effects.sp" +#include "rytp_horror/playergroups.sp" +#include "rytp_horror/menus.sp" +#include "rytp_horror/pvp.sp" +#include "rytp_horror/client.sp" +#include "rytp_horror/npc.sp" +#include "rytp_horror/specialround.sp" +#include "rytp_horror/adminmenu.sp" + + +#define SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND "ui/item_acquired.wav" + +// ========================================================== +// GENERAL PLUGIN HOOK FUNCTIONS +// ========================================================== + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + RegPluginLibrary("sf2"); + + fOnBossAdded = CreateGlobalForward("SF2_OnBossAdded", ET_Ignore, Param_Cell); + fOnBossSpawn = CreateGlobalForward("SF2_OnBossSpawn", ET_Ignore, Param_Cell); + fOnBossChangeState = CreateGlobalForward("SF2_OnBossChangeState", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + fOnBossRemoved = CreateGlobalForward("SF2_OnBossRemoved", ET_Ignore, Param_Cell); + fOnPagesSpawned = CreateGlobalForward("SF2_OnPagesSpawned", ET_Ignore); + fOnClientBlink = CreateGlobalForward("SF2_OnClientBlink", ET_Ignore, Param_Cell); + fOnClientCaughtByBoss = CreateGlobalForward("SF2_OnClientCaughtByBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientGiveQueuePoints = CreateGlobalForward("SF2_OnClientGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientActivateFlashlight = CreateGlobalForward("SF2_OnClientActivateFlashlight", ET_Ignore, Param_Cell); + fOnClientDeactivateFlashlight = CreateGlobalForward("SF2_OnClientDeactivateFlashlight", ET_Ignore, Param_Cell); + fOnClientBreakFlashlight = CreateGlobalForward("SF2_OnClientBreakFlashlight", ET_Ignore, Param_Cell); + fOnClientEscape = CreateGlobalForward("SF2_OnClientEscape", ET_Ignore, Param_Cell); + fOnClientLooksAtBoss = CreateGlobalForward("SF2_OnClientLooksAtBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientLooksAwayFromBoss = CreateGlobalForward("SF2_OnClientLooksAwayFromBoss", ET_Ignore, Param_Cell, Param_Cell); + fOnClientStartDeathCam = CreateGlobalForward("SF2_OnClientStartDeathCam", ET_Ignore, Param_Cell, Param_Cell); + fOnClientEndDeathCam = CreateGlobalForward("SF2_OnClientEndDeathCam", ET_Ignore, Param_Cell, Param_Cell); + fOnClientGetDefaultWalkSpeed = CreateGlobalForward("SF2_OnClientGetDefaultWalkSpeed", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientGetDefaultSprintSpeed = CreateGlobalForward("SF2_OnClientGetDefaultSprintSpeed", ET_Hook, Param_Cell, Param_CellByRef); + fOnClientSpawnedAsProxy = CreateGlobalForward("SF2_OnClientSpawnedAsProxy", ET_Ignore, Param_Cell); + fOnClientDamagedByBoss = CreateGlobalForward("SF2_OnClientDamagedByBoss", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell); + fOnGroupGiveQueuePoints = CreateGlobalForward("SF2_OnGroupGiveQueuePoints", ET_Hook, Param_Cell, Param_CellByRef); + + CreateNative("SF2_IsRunning", Native_IsRunning); + CreateNative("SF2_GetCurrentDifficulty", Native_GetCurrentDifficulty); + CreateNative("SF2_GetDifficultyModifier", Native_GetDifficultyModifier); + CreateNative("SF2_IsClientEliminated", Native_IsClientEliminated); + CreateNative("SF2_IsClientInGhostMode", Native_IsClientInGhostMode); + CreateNative("SF2_IsClientProxy", Native_IsClientProxy); + CreateNative("SF2_GetClientBlinkCount", Native_GetClientBlinkCount); + CreateNative("SF2_GetClientProxyMaster", Native_GetClientProxyMaster); + CreateNative("SF2_GetClientProxyControlAmount", Native_GetClientProxyControlAmount); + CreateNative("SF2_GetClientProxyControlRate", Native_GetClientProxyControlRate); + CreateNative("SF2_SetClientProxyMaster", Native_SetClientProxyMaster); + CreateNative("SF2_SetClientProxyControlAmount", Native_SetClientProxyControlAmount); + CreateNative("SF2_SetClientProxyControlRate", Native_SetClientProxyControlRate); + CreateNative("SF2_IsClientLookingAtBoss", Native_IsClientLookingAtBoss); + CreateNative("SF2_CollectAsPage", Native_CollectAsPage); + CreateNative("SF2_GetMaxBossCount", Native_GetMaxBosses); + CreateNative("SF2_EntIndexToBossIndex", Native_EntIndexToBossIndex); + CreateNative("SF2_BossIndexToEntIndex", Native_BossIndexToEntIndex); + CreateNative("SF2_BossIDToBossIndex", Native_BossIDToBossIndex); + CreateNative("SF2_BossIndexToBossID", Native_BossIndexToBossID); + CreateNative("SF2_GetBossName", Native_GetBossName); + CreateNative("SF2_GetBossModelEntity", Native_GetBossModelEntity); + CreateNative("SF2_GetBossTarget", Native_GetBossTarget); + CreateNative("SF2_GetBossMaster", Native_GetBossMaster); + CreateNative("SF2_GetBossState", Native_GetBossState); + CreateNative("SF2_IsBossProfileValid", Native_IsBossProfileValid); + CreateNative("SF2_GetBossProfileNum", Native_GetBossProfileNum); + CreateNative("SF2_GetBossProfileFloat", Native_GetBossProfileFloat); + CreateNative("SF2_GetBossProfileString", Native_GetBossProfileString); + CreateNative("SF2_GetBossProfileVector", Native_GetBossProfileVector); + CreateNative("SF2_GetRandomStringFromBossProfile", Native_GetRandomStringFromBossProfile); + + PvP_InitializeAPI(); + + SpecialRoundInitializeAPI(); + + return APLRes_Success; +} + +public OnPluginStart() +{ + LoadTranslations("core.phrases"); + LoadTranslations("common.phrases"); + LoadTranslations("sf2.phrases"); + + // Get offsets. + g_offsPlayerFOV = FindSendPropInfo("CBasePlayer", "m_iFOV"); + if (g_offsPlayerFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iFOV."); + + g_offsPlayerDefaultFOV = FindSendPropInfo("CBasePlayer", "m_iDefaultFOV"); + if (g_offsPlayerDefaultFOV == -1) SetFailState("Couldn't find CBasePlayer offset for m_iDefaultFOV."); + + g_offsPlayerFogCtrl = FindSendPropInfo("CBasePlayer", "m_PlayerFog.m_hCtrl"); + if (g_offsPlayerFogCtrl == -1) LogError("Couldn't find CBasePlayer offset for m_PlayerFog.m_hCtrl!"); + + g_offsPlayerPunchAngle = FindSendPropInfo("CBasePlayer", "m_vecPunchAngle"); + if (g_offsPlayerPunchAngle == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngle!"); + + g_offsPlayerPunchAngleVel = FindSendPropInfo("CBasePlayer", "m_vecPunchAngleVel"); + if (g_offsPlayerPunchAngleVel == -1) LogError("Couldn't find CBasePlayer offset for m_vecPunchAngleVel!"); + + g_offsFogCtrlEnable = FindSendPropInfo("CFogController", "m_fog.enable"); + if (g_offsFogCtrlEnable == -1) LogError("Couldn't find CFogController offset for m_fog.enable!"); + + g_offsFogCtrlEnd = FindSendPropInfo("CFogController", "m_fog.end"); + if (g_offsFogCtrlEnd == -1) LogError("Couldn't find CFogController offset for m_fog.end!"); + + g_hPageMusicRanges = CreateArray(3); + + // Register console variables. + g_cvVersion = CreateConVar("sf2_version", PLUGIN_VERSION, "The current version of Slender Fortress. DO NOT TOUCH!", FCVAR_SPONLY | FCVAR_NOTIFY | FCVAR_DONTRECORD); + SetConVarString(g_cvVersion, PLUGIN_VERSION); + + g_cvEnabled = CreateConVar("sf2_enabled", "1", "Enable/Disable the Slender Fortress gamemode. This will take effect on map change.", FCVAR_NOTIFY | FCVAR_DONTRECORD); + g_cvSlenderMapsOnly = CreateConVar("sf2_slendermapsonly", "1", "Only enable the Slender Fortress gamemode on map names prefixed with \"slender_\" or \"sf2_\"."); + + g_cvGraceTime = CreateConVar("sf2_gracetime", "30.0"); + g_cvIntroEnabled = CreateConVar("sf2_intro_enabled", "1"); + g_cvIntroDefaultHoldTime = CreateConVar("sf2_intro_default_hold_time", "9.0"); + g_cvIntroDefaultFadeTime = CreateConVar("sf2_intro_default_fade_time", "1.0"); + + g_cvBlockSuicideDuringRound = CreateConVar("sf2_block_suicide_during_round", "0"); + + g_cvAllChat = CreateConVar("sf2_alltalk", "0"); + HookConVarChange(g_cvAllChat, OnConVarChanged); + + g_cvPlayerVoiceDistance = CreateConVar("sf2_player_voice_distance", "800.0", "The maximum distance RED can communicate in voice chat. Set to 0 if you want them to be heard at all times.", _, true, 0.0); + g_cvPlayerVoiceWallScale = CreateConVar("sf2_player_voice_scale_blocked", "0.5", "The distance required to hear RED in voice chat will be multiplied by this amount if something is blocking them."); + + g_cvPlayerViewbobEnabled = CreateConVar("sf2_player_viewbob_enabled", "1", "Enable/Disable player viewbobbing.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobEnabled, OnConVarChanged); + g_cvPlayerViewbobHurtEnabled = CreateConVar("sf2_player_viewbob_hurt_enabled", "0", "Enable/Disable player view tilting when hurt.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobHurtEnabled, OnConVarChanged); + g_cvPlayerViewbobSprintEnabled = CreateConVar("sf2_player_viewbob_sprint_enabled", "0", "Enable/Disable player step viewbobbing when sprinting.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerViewbobSprintEnabled, OnConVarChanged); + g_cvGravity = FindConVar("sv_gravity"); + HookConVarChange(g_cvGravity, OnConVarChanged); + + g_cvPlayerFakeLagCompensation = CreateConVar("sf2_player_fakelagcompensation", "0", "(EXPERIMENTAL) Enable/Disable fake lag compensation for some hitscan weapons such as the Sniper Rifle.", _, true, 0.0, true, 1.0); + + g_cvPlayerShakeEnabled = CreateConVar("sf2_player_shake_enabled", "1", "Enable/Disable player view shake during boss encounters.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cvPlayerShakeEnabled, OnConVarChanged); + g_cvPlayerShakeFrequencyMax = CreateConVar("sf2_player_shake_frequency_max", "255", "Maximum frequency value of the shake. Should be a value between 1-255.", _, true, 1.0, true, 255.0); + g_cvPlayerShakeAmplitudeMax = CreateConVar("sf2_player_shake_amplitude_max", "5", "Maximum amplitude value of the shake. Should be a value between 1-16.", _, true, 1.0, true, 16.0); + + g_cvPlayerBlinkRate = CreateConVar("sf2_player_blink_rate", "0.33", "How long (in seconds) each bar on the player's Blink meter lasts.", _, true, 0.0); + g_cvPlayerBlinkHoldTime = CreateConVar("sf2_player_blink_holdtime", "0.15", "How long (in seconds) a player will stay in Blink mode when he or she blinks.", _, true, 0.0); + + g_cvUltravisionEnabled = CreateConVar("sf2_player_ultravision_enabled", "1", "Enable/Disable player Ultravision. This helps players see in the dark when their Flashlight is off or unavailable.", _, true, 0.0, true, 1.0); + g_cvUltravisionRadiusRed = CreateConVar("sf2_player_ultravision_radius_red", "512.0"); + g_cvUltravisionRadiusBlue = CreateConVar("sf2_player_ultravision_radius_blue", "800.0"); + g_cvUltravisionBrightness = CreateConVar("sf2_player_ultravision_brightness", "-4"); + + g_cvGhostModeConnectionCheck = CreateConVar("sf2_ghostmode_check_connection", "1", "Checks a player's connection while in Ghost Mode. If the check fails, the client is booted out of Ghost Mode and the action and client's SteamID is logged in the main SF2 log."); + g_cvGhostModeConnectionTolerance = CreateConVar("sf2_ghostmode_connection_tolerance", "5.0", "If sf2_ghostmode_check_connection is set to 1 and the client has timed out for at least this amount of time, the client will be booted out of Ghost Mode."); + + g_cv20Dollars = CreateConVar("sf2_20dollarmode", "0", "Enable/Disable $20 mode.", _, true, 0.0, true, 1.0); + HookConVarChange(g_cv20Dollars, OnConVarChanged); + + g_cvMaxPlayers = CreateConVar("sf2_maxplayers", "5", "The maximum amount of players that can be in one round.", _, true, 1.0); + HookConVarChange(g_cvMaxPlayers, OnConVarChanged); + + g_cvMaxPlayersOverride = CreateConVar("sf2_maxplayers_override", "-1", "Overrides the maximum amount of players that can be in one round.", _, true, -1.0); + HookConVarChange(g_cvMaxPlayersOverride, OnConVarChanged); + + g_cvCampingEnabled = CreateConVar("sf2_anticamping_enabled", "1", "Enable/Disable anti-camping system for RED.", _, true, 0.0, true, 1.0); + g_cvCampingMaxStrikes = CreateConVar("sf2_anticamping_maxstrikes", "4", "How many 5-second intervals players are allowed to stay in one spot before he/she is forced to suicide.", _, true, 0.0); + g_cvCampingStrikesWarn = CreateConVar("sf2_anticamping_strikeswarn", "2", "The amount of strikes left where the player will be warned of camping."); + g_cvCampingMinDistance = CreateConVar("sf2_anticamping_mindistance", "128.0", "Every 5 seconds the player has to be at least this far away from his last position 5 seconds ago or else he'll get a strike."); + g_cvCampingNoStrikeSanity = CreateConVar("sf2_anticamping_no_strike_sanity", "0.1", "The camping system will NOT give any strikes under any circumstances if the players's Sanity is missing at least this much of his maximum Sanity (max is 1.0)."); + g_cvCampingNoStrikeBossDistance = CreateConVar("sf2_anticamping_no_strike_boss_distance", "512.0", "The camping system will NOT give any strikes under any circumstances if the player is this close to a boss (ignoring LOS)."); + g_cvBossMain = CreateConVar("sf2_boss_main", "slenderman", "The name of the main boss (its profile name, not its display name)"); + g_cvBossProfileOverride = CreateConVar("sf2_boss_profile_override", "", "Overrides which boss will be chosen next. Only applies to the first boss being chosen."); + g_cvDifficulty = CreateConVar("sf2_difficulty", "1", "Difficulty of the game. 1 = Normal, 2 = Hard, 3 = Insane.", _, true, 1.0, true, 3.0); + HookConVarChange(g_cvDifficulty, OnConVarChanged); + + g_cvSpecialRoundBehavior = CreateConVar("sf2_specialround_mode", "0", "0 = Special Round resets on next round, 1 = Special Round keeps going until all players have played (not counting spectators, recently joined players, and those who reset their queue points during the round)", _, true, 0.0, true, 1.0); + g_cvSpecialRoundForce = CreateConVar("sf2_specialround_forceenable", "-1", "Sets whether a Special Round will occur on the next round or not.", _, true, -1.0, true, 1.0); + g_cvSpecialRoundOverride = CreateConVar("sf2_specialround_forcetype", "-1", "Sets the type of Special Round that will be chosen on the next Special Round. Set to -1 to let the game choose.", _, true, -1.0); + g_cvSpecialRoundInterval = CreateConVar("sf2_specialround_interval", "5", "If this many rounds are completed, the next round will be a Special Round.", _, true, 0.0); + + g_cvNewBossRoundBehavior = CreateConVar("sf2_newbossround_mode", "0", "0 = boss selection will return to normal after the boss round, 1 = the new boss will continue being the boss until all players in the server have played against it (not counting spectators, recently joined players, and those who reset their queue points during the round).", _, true, 0.0, true, 1.0); + g_cvNewBossRoundInterval = CreateConVar("sf2_newbossround_interval", "3", "If this many rounds are completed, the next round's boss will be randomly chosen, but will not be the main boss.", _, true, 0.0); + g_cvNewBossRoundForce = CreateConVar("sf2_newbossround_forceenable", "-1", "Sets whether a new boss will be chosen on the next round or not. Set to -1 to let the game choose.", _, true, -1.0, true, 1.0); + + g_cvTimeLimit = CreateConVar("sf2_timelimit_default", "300", "The time limit of the round. Maps can change the time limit.", _, true, 0.0); + g_cvTimeLimitEscape = CreateConVar("sf2_timelimit_escape_default", "90", "The time limit to escape. Maps can change the time limit.", _, true, 0.0); + g_cvTimeGainFromPageGrab = CreateConVar("sf2_time_gain_page_grab", "12", "The time gained from grabbing a page. Maps can change the time gain amount."); + + g_cvWarmupRound = CreateConVar("sf2_warmupround", "1", "Enables/disables Warmup Rounds after the \"Waiting for Players\" phase.", _, true, 0.0, true, 1.0); + g_cvWarmupRoundNum = CreateConVar("sf2_warmupround_num", "1", "Sets the amount of Warmup Rounds that occur after the \"Waiting for Players\" phase.", _, true, 0.0); + + g_cvPlayerProxyWaitTime = CreateConVar("sf2_player_proxy_waittime", "35", "How long (in seconds) after a player was chosen to be a Proxy must the system wait before choosing him again."); + g_cvPlayerProxyAsk = CreateConVar("sf2_player_proxy_ask", "0", "Set to 1 if the player can choose before becoming a Proxy, set to 0 to force."); + + g_cvHalfZatoichiHealthGain = CreateConVar("sf2_halfzatoichi_healthgain", "20", "How much health should be gained from killing a player with the Half-Zatoichi? Set to -1 for default behavior."); + + g_cvPlayerInfiniteSprintOverride = CreateConVar("sf2_player_infinite_sprint_override", "-1", "1 = infinite sprint, 0 = never have infinite sprint, -1 = let the game choose.", _, true, -1.0, true, 1.0); + g_cvPlayerInfiniteFlashlightOverride = CreateConVar("sf2_player_infinite_flashlight_override", "-1", "1 = infinite flashlight, 0 = never have infinite flashlight, -1 = let the game choose.", _, true, -1.0, true, 1.0); + g_cvPlayerInfiniteBlinkOverride = CreateConVar("sf2_player_infinite_blink_override", "-1", "1 = infinite blink, 0 = never have infinite blink, -1 = let the game choose.", _, true, -1.0, true, 1.0); + + g_cvMaxRounds = FindConVar("mp_maxrounds"); + + g_hHudSync = CreateHudSynchronizer(); + g_hHudSync2 = CreateHudSynchronizer(); + g_hRoundTimerSync = CreateHudSynchronizer(); + g_hCookie = RegClientCookie("slender_cookie", "", CookieAccess_Private); + + // Register console commands. + RegConsoleCmd("sm_sf2", Command_MainMenu); + RegConsoleCmd("sm_slender", Command_MainMenu); + RegConsoleCmd("sm_horror", Command_MainMenu); + RegConsoleCmd("sm_slnext", Command_Next); + RegConsoleCmd("sm_slgroup", Command_Group); + RegConsoleCmd("sm_slgroupname", Command_GroupName); + RegConsoleCmd("sm_slghost", Command_GhostMode); + RegConsoleCmd("sm_slhelp", Command_Help); + RegConsoleCmd("sm_slsettings", Command_Settings); + RegConsoleCmd("sm_slcredits", Command_Credits); + RegConsoleCmd("sm_flashlight", Command_ToggleFlashlight); + RegConsoleCmd("+sprint", Command_SprintOn); + RegConsoleCmd("-sprint", Command_SprintOff); + + RegAdminCmd("sm_sf2_scare", Command_ClientPerformScare, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_spawn_boss", Command_SpawnSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_add_boss", Command_AddSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_add_boss_fake", Command_AddSlenderFake, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_remove_boss", Command_RemoveSlender, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_getbossindexes", Command_GetBossIndexes, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_setplaystate", Command_ForceState, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_boss_attack_waiters", Command_SlenderAttackWaiters, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_boss_no_teleport", Command_SlenderNoTeleport, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_force_proxy", Command_ForceProxy, ADMFLAG_SLAY); + RegAdminCmd("sm_sf2_force_escape", Command_ForceEscape, ADMFLAG_CHEATS); + + // Hook onto existing console commands. + AddCommandListener(Hook_CommandBuild, "build"); + AddCommandListener(Hook_CommandSuicideAttempt, "kill"); + AddCommandListener(Hook_CommandSuicideAttempt, "explode"); + AddCommandListener(Hook_CommandSuicideAttempt, "joinclass"); + AddCommandListener(Hook_CommandSuicideAttempt, "join_class"); + AddCommandListener(Hook_CommandSuicideAttempt, "jointeam"); + AddCommandListener(Hook_CommandSuicideAttempt, "spectate"); + AddCommandListener(Hook_CommandVoiceMenu, "voicemenu"); + AddCommandListener(Hook_CommandSay, "say"); + + // Hook events. + HookEvent("teamplay_round_start", Event_RoundStart); + HookEvent("teamplay_round_win", Event_RoundEnd); + HookEvent("player_team", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("player_team", Event_PlayerTeam); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("post_inventory_application", Event_PostInventoryApplication); + HookEvent("item_found", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("teamplay_teambalanced_player", Event_DontBroadcastToClients, EventHookMode_Pre); + HookEvent("fish_notice", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("fish_notice__arm", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeathPre, EventHookMode_Pre); + HookEvent("player_death", Event_PlayerDeath); + + // Hook entities. + HookEntityOutput("trigger_multiple", "OnStartTouch", Hook_TriggerOnStartTouch); + HookEntityOutput("trigger_multiple", "OnEndTouch", Hook_TriggerOnEndTouch); + + // Hook usermessages. + HookUserMessage(GetUserMessageId("VoiceSubtitle"), Hook_BlockUserMessage, true); + + // Hook sounds. + AddNormalSoundHook(Hook_NormalSound); + + AddTempEntHook("Fire Bullets", Hook_TEFireBullets); + + InitializeBossProfiles(); + + NPCInitialize(); + + SetupMenus(); + + SetupAdminMenu(); + + SetupClassDefaultWeapons(); + + SetupPlayerGroups(); + + PvP_Initialize(); + + // @TODO: When cvars are finalized, set this to true. + AutoExecConfig(false); + +#if defined DEBUG + InitializeDebug(); +#endif +} + +public OnAllPluginsLoaded() +{ + SetupHooks(); +} + +public OnPluginEnd() +{ + for(new c = 1; c < MaxClients; c++) DestroySpriteOverlay(c); + StopPlugin(); +} + +static SetupHooks() +{ + // Check SDKHooks gamedata. + new Handle:hConfig = LoadGameConfigFile("sdkhooks.games"); + if (hConfig == INVALID_HANDLE) SetFailState("Couldn't find SDKHooks gamedata!"); + + StartPrepSDKCall(SDKCall_Entity); + PrepSDKCall_SetFromConf(hConfig, SDKConf_Virtual, "GetMaxHealth"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + if ((g_hSDKGetMaxHealth = EndPrepSDKCall()) == INVALID_HANDLE) + { + SetFailState("Failed to retrieve GetMaxHealth offset from SDKHooks gamedata!"); + } + + CloseHandle(hConfig); + + // Check our own gamedata. + hConfig = LoadGameConfigFile("sf2"); + if (hConfig == INVALID_HANDLE) SetFailState("Could not find SF2 gamedata!"); + + new iOffset = GameConfGetOffset(hConfig, "CTFPlayer::WantsLagCompensationOnEntity"); + g_hSDKWantsLagCompensationOnEntity = DHookCreate(iOffset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, Hook_ClientWantsLagCompensationOnEntity); + if (g_hSDKWantsLagCompensationOnEntity == INVALID_HANDLE) + { + SetFailState("Failed to create hook CTFPlayer::WantsLagCompensationOnEntity offset from SF2 gamedata!"); + } + + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_CBaseEntity); + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_ObjectPtr); + DHookAddParam(g_hSDKWantsLagCompensationOnEntity, HookParamType_Unknown); + + iOffset = GameConfGetOffset(hConfig, "CBaseEntity::ShouldTransmit"); + g_hSDKShouldTransmit = DHookCreate(iOffset, HookType_Entity, ReturnType_Int, ThisPointer_CBaseEntity, Hook_EntityShouldTransmit); + if (g_hSDKShouldTransmit == INVALID_HANDLE) + { + SetFailState("Failed to create hook CBaseEntity::ShouldTransmit offset from SF2 gamedata!"); + } + + DHookAddParam(g_hSDKShouldTransmit, HookParamType_ObjectPtr); + + CloseHandle(hConfig); +} + +static SetupClassDefaultWeapons() +{ + // Scout + g_hSDKWeaponScattergun = PrepareItemHandle("tf_weapon_scattergun", 13, 0, 0, ""); + g_hSDKWeaponPistolScout = PrepareItemHandle("tf_weapon_pistol", 23, 0, 0, ""); + g_hSDKWeaponBat = PrepareItemHandle("tf_weapon_bat", 0, 0, 0, ""); + + // Sniper + g_hSDKWeaponSniperRifle = PrepareItemHandle("tf_weapon_sniperrifle", 14, 0, 0, ""); + g_hSDKWeaponSMG = PrepareItemHandle("tf_weapon_smg", 16, 0, 0, ""); + g_hSDKWeaponKukri = PrepareItemHandle("tf_weapon_club", 3, 0, 0, ""); + + // Soldier + g_hSDKWeaponRocketLauncher = PrepareItemHandle("tf_weapon_rocketlauncher", 18, 0, 0, ""); + g_hSDKWeaponShotgunSoldier = PrepareItemHandle("tf_weapon_shotgun", 10, 0, 0, ""); + g_hSDKWeaponShovel = PrepareItemHandle("tf_weapon_shovel", 6, 0, 0, ""); + + // Demoman + g_hSDKWeaponGrenadeLauncher = PrepareItemHandle("tf_weapon_grenadelauncher", 19, 0, 0, ""); + g_hSDKWeaponStickyLauncher = PrepareItemHandle("tf_weapon_pipebomblauncher", 20, 0, 0, ""); + g_hSDKWeaponBottle = PrepareItemHandle("tf_weapon_bottle", 1, 0, 0, ""); + + // Heavy + g_hSDKWeaponMinigun = PrepareItemHandle("tf_weapon_minigun", 15, 0, 0, ""); + g_hSDKWeaponShotgunHeavy = PrepareItemHandle("tf_weapon_shotgun", 11, 0, 0, ""); + g_hSDKWeaponFists = PrepareItemHandle("tf_weapon_fists", 5, 0, 0, ""); + + // Medic + g_hSDKWeaponSyringeGun = PrepareItemHandle("tf_weapon_syringegun_medic", 17, 0, 0, ""); + g_hSDKWeaponMedigun = PrepareItemHandle("tf_weapon_medigun", 29, 0, 0, ""); + g_hSDKWeaponBonesaw = PrepareItemHandle("tf_weapon_bonesaw", 8, 0, 0, ""); + + // Pyro + g_hSDKWeaponFlamethrower = PrepareItemHandle("tf_weapon_flamethrower", 21, 0, 0, "254 ; 4.0"); + g_hSDKWeaponShotgunPyro = PrepareItemHandle("tf_weapon_shotgun", 12, 0, 0, ""); + g_hSDKWeaponFireaxe = PrepareItemHandle("tf_weapon_fireaxe", 2, 0, 0, ""); + + // Spy + g_hSDKWeaponRevolver = PrepareItemHandle("tf_weapon_revolver", 24, 0, 0, ""); + g_hSDKWeaponKnife = PrepareItemHandle("tf_weapon_knife", 4, 0, 0, ""); + g_hSDKWeaponInvis = PrepareItemHandle("tf_weapon_invis", 297, 0, 0, ""); + + // Engineer + g_hSDKWeaponShotgunPrimary = PrepareItemHandle("tf_weapon_shotgun", 9, 0, 0, ""); + g_hSDKWeaponPistol = PrepareItemHandle("tf_weapon_pistol", 22, 0, 0, ""); + g_hSDKWeaponWrench = PrepareItemHandle("tf_weapon_wrench", 7, 0, 0, ""); +} + +public OnMapStart() +{ + PvP_OnMapStart(); +} + +public OnConfigsExecuted() +{ + if (!GetConVarBool(g_cvEnabled)) + { + StopPlugin(); + } + else + { + if (GetConVarBool(g_cvSlenderMapsOnly)) + { + decl String:sMap[256]; + GetCurrentMap(sMap, sizeof(sMap)); + + if (!StrContains(sMap, "slender_", false) || !StrContains(sMap, "sf2_", false)) + { + StartPlugin(); + } + else + { + LogMessage("%s is not a Slender Fortress map. Plugin disabled!", sMap); + StopPlugin(); + } + } + else + { + StartPlugin(); + } + } +} + +static StartPlugin() +{ + if (g_bEnabled) return; + + g_bEnabled = true; + + InitializeLogging(); + +#if defined DEBUG + InitializeDebugLogging(); +#endif + + // Handle ConVars. + new Handle:hCvar = FindConVar("mp_friendlyfire"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mp_flashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mat_supportflashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, true); + + hCvar = FindConVar("mp_autoteambalance"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + g_flGravity = GetConVarFloat(g_cvGravity); + + g_b20Dollars = GetConVarBool(g_cv20Dollars); + + g_bPlayerShakeEnabled = GetConVarBool(g_cvPlayerShakeEnabled); + g_bPlayerViewbobEnabled = GetConVarBool(g_cvPlayerViewbobEnabled); + g_bPlayerViewbobHurtEnabled = GetConVarBool(g_cvPlayerViewbobHurtEnabled); + g_bPlayerViewbobSprintEnabled = GetConVarBool(g_cvPlayerViewbobSprintEnabled); + + decl String:sBuffer[64]; + Format(sBuffer, sizeof(sBuffer), "RYTP Horror", PLUGIN_VERSION_DISPLAY); + Steam_SetGameDescription(sBuffer); + + PrecacheStuff(); + + // Reset special round. + g_bSpecialRound = false; + g_bSpecialRoundNew = false; + g_bSpecialRoundContinuous = false; + g_iSpecialRoundCount = 1; + g_iSpecialRoundType = 0; + + SpecialRoundReset(); + + // Reset boss rounds. + g_bNewBossRound = false; + g_bNewBossRoundNew = false; + g_bNewBossRoundContinuous = false; + g_iNewBossRoundCount = 1; + strcopy(g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile), ""); + + // Reset global round vars. + g_iRoundCount = 0; + g_iRoundEndCount = 0; + g_iRoundActiveCount = 0; + g_iRoundState = SF2RoundState_Invalid; + g_hRoundMessagesTimer = CreateTimer(200.0, Timer_RoundMessages, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_iRoundMessagesNum = 0; + + g_iRoundWarmupRoundCount = 0; + + g_hClientAverageUpdateTimer = CreateTimer(0.2, Timer_ClientAverageUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_hBossCountUpdateTimer = CreateTimer(2.0, Timer_BossCountUpdate, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + SetRoundState(SF2RoundState_Waiting); + + ReloadBossProfiles(); + ReloadRestrictedWeapons(); + ReloadSpecialRounds(); + + NPCOnConfigsExecuted(); + + InitializeBossPackVotes(); + SetupTimeLimitTimerForBossPackVote(); + + // Late load compensation. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + OnClientPutInServer(i); + } +} + +static PrecacheStuff() +{ + // Initialize particles. + g_iParticleCriticalHit = PrecacheParticleSystem(CRIT_PARTICLENAME); + + PrecacheSound2(CRIT_SOUND); + + // simple_bot; + PrecacheModel("models/humans/group01/female_01.mdl", true); + + PrecacheModel(PAGE_MODEL, true); + PrecacheModel(GHOST_MODEL, true); + + PrecacheSound2(FLASHLIGHT_CLICKSOUND); + PrecacheSound2(FLASHLIGHT_BREAKSOUND); + PrecacheSound2(FLASHLIGHT_NOSOUND); + PrecacheSound2(PAGE_GRABSOUND); + + PrecacheSound2(MUSIC_GOTPAGES1_SOUND); + PrecacheSound2(MUSIC_GOTPAGES2_SOUND); + PrecacheSound2(MUSIC_GOTPAGES3_SOUND); + PrecacheSound2(MUSIC_GOTPAGES4_SOUND); + + PrecacheSound2(SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); + + for (new i = 0; i < sizeof(g_strPlayerBreathSounds); i++) + { + PrecacheSound2(g_strPlayerBreathSounds[i]); + } + + for (new i = 0; i < sizeof(g_strGhostHelpPhrases); i++) + { + PrecacheSound2(g_strGhostHelpPhrases[i]); + } + + for (new i = 0; i < sizeof(g_sOverlayMat); i++) + { + new String:path[PLATFORM_MAX_PATH]; + Format(path, sizeof(path), "%s.vmt", g_sOverlayMat[i]); + PrecacheModel(path); + Format(path, sizeof(path), "%s.vtf", g_sOverlayMat[i]); + PrecacheModel(path); + } + PrecacheMaterial2(STATIC_OVERLAY); + PrecacheSound2(STATIC_SOUND); + + // Special round. + PrecacheSound2(SR_MUSIC); + PrecacheSound2(SR_SOUND_SELECT); + PrecacheSound2(SF2_INTRO_DEFAULT_MUSIC); + + PrecacheMaterial2(SF2_OVERLAY_DEFAULT); + PrecacheMaterial2(SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); + PrecacheMaterial2(SF2_OVERLAY_GHOST); + + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.mdl"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx80.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.dx90.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.phy"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.sw.vtx"); + AddFileToDownloadsTable("models/rytp/horror/props/hint_paper.vvd"); + + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_1.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_2.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_3.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_4.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_5.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_6.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_7.vmt"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vtf"); + AddFileToDownloadsTable("materials/models/rytp/horror/props/hint_paper/paper_8.vmt"); + + // pvp + PvP_Precache(); +} + +static StopPlugin() +{ + if (!g_bEnabled) return; + + g_bEnabled = false; + + // Reset CVars. + new Handle:hCvar = FindConVar("mp_friendlyfire"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + hCvar = FindConVar("mp_flashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + hCvar = FindConVar("mat_supportflashlight"); + if (hCvar != INVALID_HANDLE) SetConVarBool(hCvar, false); + + // Cleanup bosses. + NPCRemoveAll(); + + // Cleanup clients. + for (new i = 1; i <= MaxClients; i++) + { + ClientResetFlashlight(i); + ClientDeactivateUltravision(i); + ClientDisableConstantGlow(i); + ClientRemoveInteractiveGlow(i); + } + + BossProfilesOnMapEnd(); +} + +public OnMapEnd() +{ + StopPlugin(); +} + +public OnMapTimeLeftChanged() +{ + if (g_bEnabled) + { + SetupTimeLimitTimerForBossPackVote(); + } +} + +public TF2_OnConditionAdded(client, TFCond:cond) +{ + if (cond == TFCond_Taunting) + { + if (IsClientInGhostMode(client)) + { + // Stop ghosties from taunting. + TF2_RemoveCondition(client, TFCond_Taunting); + } + } +} + +public OnGameFrame() +{ + if (!g_bEnabled) return; + + // Process through boss movement. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new iBoss = NPCGetEntIndex(i); + if (!iBoss || iBoss == INVALID_ENT_REFERENCE) continue; + + if (NPCGetFlags(i) & SFF_MARKEDASFAKE) continue; + + new iType = NPCGetType(i); + + switch (iType) + { + case SF2BossType_Static: + { + decl Float:myPos[3], Float:hisPos[3]; + SlenderGetAbsOrigin(i, myPos); + AddVectors(myPos, g_flSlenderEyePosOffset[i], myPos); + + new iBestPlayer = -1; + new Float:flBestDistance = 16384.0; + new Float:flTempDistance; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !IsPlayerAlive(iClient) || IsClientInGhostMode(iClient) || IsClientInDeathCam(iClient)) continue; + if (!IsPointVisibleToPlayer(iClient, myPos, false, false)) continue; + + GetClientAbsOrigin(iClient, hisPos); + + flTempDistance = GetVectorDistance(myPos, hisPos); + if (flTempDistance < flBestDistance) + { + iBestPlayer = iClient; + flBestDistance = flTempDistance; + } + } + + if (iBestPlayer > 0) + { + SlenderGetAbsOrigin(i, myPos); + GetClientAbsOrigin(iBestPlayer, hisPos); + + if (!SlenderOnlyLooksIfNotSeen(i) || !IsPointVisibleToAPlayer(myPos, false, SlenderUsesBlink(i))) + { + new Float:flTurnRate = NPCGetTurnRate(i); + + if (flTurnRate > 0.0) + { + decl Float:flMyEyeAng[3], Float:ang[3]; + GetEntPropVector(iBoss, Prop_Data, "m_angAbsRotation", flMyEyeAng); + AddVectors(flMyEyeAng, g_flSlenderEyeAngOffset[i], flMyEyeAng); + SubtractVectors(hisPos, myPos, ang); + GetVectorAngles(ang, ang); + ang[0] = 0.0; + ang[1] += (AngleDiff(ang[1], flMyEyeAng[1]) >= 0.0 ? 1.0 : -1.0) * flTurnRate * GetTickInterval(); + ang[2] = 0.0; + + // Take care of angle offsets. + AddVectors(ang, g_flSlenderEyePosOffset[i], ang); + for (new i2 = 0; i2 < 3; i2++) ang[i2] = AngleNormalize(ang[i2]); + + TeleportEntity(iBoss, NULL_VECTOR, ang, NULL_VECTOR); + } + } + } + } + case SF2BossType_Chaser: + { + SlenderChaseBossProcessMovement(i); + } + } + } + + PvP_OnGameFrame(); +} + +// ========================================================== +// COMMANDS AND COMMAND HOOK FUNCTIONS +// ========================================================== + +public Action:Command_Help(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuHelp, client, 30); + return Plugin_Handled; +} + +public Action:Command_Settings(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuSettings, client, 30); + return Plugin_Handled; +} + +public Action:Command_Credits(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuCredits, client, MENU_TIME_FOREVER); + return Plugin_Handled; +} + +public Action:Command_ToggleFlashlight(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return Plugin_Handled; + + if (!IsRoundInWarmup() && !IsRoundInIntro() && !IsRoundEnding() && !DidClientEscape(client)) + { + if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) + { + ClientHandleFlashlight(client); + } + } + + return Plugin_Handled; +} + +public Action:Command_SprintOn(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) + { + ClientHandleSprint(client, true); + } + + return Plugin_Handled; +} + +public Action:Command_SprintOff(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsPlayerAlive(client) && !g_bPlayerEliminated[client]) + { + ClientHandleSprint(client, false); + } + + return Plugin_Handled; +} + +public Action:Command_MainMenu(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuMain, client, 30); + return Plugin_Handled; +} + +public Action:Command_Next(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayQueuePointsMenu(client); + return Plugin_Handled; +} + +public Action:Command_Group(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayGroupMainMenuToClient(client); + return Plugin_Handled; +} + +public Action:Command_GroupName(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_slgroupname <name>"); + return Plugin_Handled; + } + + new iGroupIndex = ClientGetPlayerGroup(client); + if (!IsPlayerGroupActive(iGroupIndex)) + { + CPrintToChat(client, "%T", "SF2 Group Does Not Exist", client); + return Plugin_Handled; + } + + if (GetPlayerGroupLeader(iGroupIndex) != client) + { + CPrintToChat(client, "%T", "SF2 Not Group Leader", client); + return Plugin_Handled; + } + + decl String:sGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetCmdArg(1, sGroupName, sizeof(sGroupName)); + if (!sGroupName[0]) + { + CPrintToChat(client, "%T", "SF2 Invalid Group Name", client); + return Plugin_Handled; + } + + decl String:sOldGroupName[SF2_MAX_PLAYER_GROUP_NAME_LENGTH]; + GetPlayerGroupName(iGroupIndex, sOldGroupName, sizeof(sOldGroupName)); + SetPlayerGroupName(iGroupIndex, sGroupName); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i)) continue; + if (ClientGetPlayerGroup(i) != iGroupIndex) continue; + CPrintToChat(i, "%T", "SF2 Group Name Set", i, sOldGroupName, sGroupName); + } + + return Plugin_Handled; +} + +public Action:Command_GhostMode(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + DisplayMenu(g_hMenuGhostMode, client, 15); + return Plugin_Handled; +} + +public Action:Hook_CommandSay(client, const String:command[], argc) +{ + if (!g_bEnabled || GetConVarBool(g_cvAllChat)) return Plugin_Continue; + + if (!IsRoundEnding()) + { + if (g_bPlayerEliminated[client]) + { + decl String:sMessage[256]; + GetCmdArgString(sMessage, sizeof(sMessage)); + FakeClientCommand(client, "say_team %s", sMessage); + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_CommandSuicideAttempt(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) return Plugin_Handled; + + if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; + + if (GetConVarBool(g_cvBlockSuicideDuringRound)) + { + if (!g_bRoundGrace && !g_bPlayerEliminated[client] && !DidClientEscape(client)) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_CommandBlockInGhostMode(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) return Plugin_Handled; + if (IsRoundInIntro() && !g_bPlayerEliminated[client]) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Hook_CommandVoiceMenu(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsClientInGhostMode(client)) + { + ClientGhostModeNextTarget(client); + return Plugin_Handled; + } + + if (g_bPlayerProxy[client]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) + { + return Plugin_Handled; + } + } + } + + return Plugin_Continue; +} + +public Action:Command_ClientPerformScare(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_scare <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32], String:arg2[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + GetCmdArg(2, arg2, sizeof(arg2)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + ClientPerformScare(target, StringToInt(arg2)); + } + + return Plugin_Handled; +} + +public Action:Command_SpawnSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args == 0) + { + ReplyToCommand(client, "Usage: sm_sf2_spawn_boss <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl Float:eyePos[3], Float:eyeAng[3], Float:endPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(endPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, endPos); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Spawned Boss", client); + LogAction(client, -1, "%N spawned boss %d! (%s)", client, iBossIndex, sProfile); + + return Plugin_Handled; +} + +public Action:Command_RemoveSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args == 0) + { + ReplyToCommand(client, "Usage: sm_sf2_remove_boss <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + NPCRemove(iBossIndex); + + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Removed Boss", client); + LogAction(client, -1, "%N removed boss %d! (%s)", client, iBossIndex, sProfile); + + return Plugin_Handled; +} + +public Action:Command_GetBossIndexes(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + decl String:sMessage[512]; + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + ClientCommand(client, "echo Active Boss Indexes:"); + ClientCommand(client, "echo ----------------------------"); + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + Format(sMessage, sizeof(sMessage), "%d - %s", i, sProfile); + if (NPCGetFlags(i) & SFF_FAKE) + { + StrCat(sMessage, sizeof(sMessage), " (fake)"); + } + + if (g_iSlenderCopyMaster[i] != -1) + { + decl String:sCat[64]; + Format(sCat, sizeof(sCat), " (copy of %d)", g_iSlenderCopyMaster[i]); + StrCat(sMessage, sizeof(sMessage), sCat); + } + + ClientCommand(client, "echo %s", sMessage); + } + + ClientCommand(client, "echo ----------------------------"); + + ReplyToCommand(client, "Printed active boss indexes to your console!"); + + return Plugin_Handled; +} + +public Action:Command_SlenderAttackWaiters(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_boss_attack_waiters <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossFlags = NPCGetFlags(iBossIndex); + + new bool:bState = bool:StringToInt(arg2); + new bool:bOldState = bool:(iBossFlags & SFF_ATTACKWAITERS); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (bState) + { + if (!bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags | SFF_ATTACKWAITERS); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Attack Waiters", client); + LogAction(client, -1, "%N forced boss %d to attack waiters! (%s)", client, iBossIndex, sProfile); + } + } + else + { + if (bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags & ~SFF_ATTACKWAITERS); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Do Not Attack Waiters", client); + LogAction(client, -1, "%N forced boss %d to not attack waiters! (%s)", client, iBossIndex, sProfile); + } + } + + return Plugin_Handled; +} + +public Action:Command_SlenderNoTeleport(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_boss_no_teleport <bossindex 0-%d> <0/1>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + new iBossIndex = StringToInt(arg1); + if (NPCGetUniqueID(iBossIndex) == -1) return Plugin_Handled; + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossFlags = NPCGetFlags(iBossIndex); + + new bool:bState = bool:StringToInt(arg2); + new bool:bOldState = bool:(iBossFlags & SFF_NOTELEPORT); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (bState) + { + if (!bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags | SFF_NOTELEPORT); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Not Teleport", client); + LogAction(client, -1, "%N disabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); + } + } + else + { + if (bOldState) + { + NPCSetFlags(iBossIndex, iBossFlags & ~SFF_NOTELEPORT); + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Boss Should Teleport", client); + LogAction(client, -1, "%N enabled teleportation of boss %d! (%s)", client, iBossIndex, sProfile); + } + } + + return Plugin_Handled; +} + +public Action:Command_ForceProxy(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_force_proxy <name|#userid> <bossindex 0-%d>", MAX_BOSSES - 1); + return Plugin_Handled; + } + + if (IsRoundEnding() || IsRoundInWarmup()) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + 0, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iBossIndex = StringToInt(arg2); + if (iBossIndex < 0 || iBossIndex >= MAX_BOSSES) + { + ReplyToCommand(client, "Boss index is out of range!"); + return Plugin_Handled; + } + else if (NPCGetUniqueID(iBossIndex) == -1) + { + ReplyToCommand(client, "Boss index is invalid! Boss index not active!"); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new iTarget = target_list[i]; + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(iTarget, sName, sizeof(sName)); + + if (!g_bPlayerEliminated[iTarget]) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Unable To Perform Action On Player In Round", client, sName); + continue; + } + + if (g_bPlayerProxy[iTarget]) continue; + + decl Float:flNewPos[3]; + + if (!SlenderCalculateNewPlace(iBossIndex, flNewPos, true, true, client)) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Player No Place For Proxy", client, sName); + continue; + } + + ClientEnableProxy(iTarget, iBossIndex); + TeleportEntity(iTarget, flNewPos, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + + LogAction(client, iTarget, "%N forced %N to be a Proxy!", client, iTarget); + } + + return Plugin_Handled; +} + +public Action:Command_ForceEscape(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_force_escape <name|#userid>"); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + COMMAND_FILTER_ALIVE, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + if (!g_bPlayerEliminated[i] && !DidClientEscape(i)) + { + ClientEscape(target); + TeleportClientToEscapePoint(target); + + LogAction(client, target, "%N forced %N to escape!", client, target); + } + } + + return Plugin_Handled; +} + +public Action:Command_AddSlender(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_add_boss <name>"); + return Plugin_Handled; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetCmdArg(1, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + if (!KvJumpToKey(g_hConfig, sProfile)) + { + ReplyToCommand(client, "That boss does not exist!"); + return Plugin_Handled; + } + + new iBossIndex = AddProfile(sProfile); + if (iBossIndex != -1) + { + decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(flPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, flPos); + + LogAction(client, -1, "%N added a boss! (%s)", client, sProfile); + } + + return Plugin_Handled; +} + +public Action:Command_AddSlenderFake(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 1) + { + ReplyToCommand(client, "Usage: sm_sf2_add_boss_fake <name>"); + return Plugin_Handled; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetCmdArg(1, sProfile, sizeof(sProfile)); + + KvRewind(g_hConfig); + if (!KvJumpToKey(g_hConfig, sProfile)) + { + ReplyToCommand(client, "That boss does not exist!"); + return Plugin_Handled; + } + + new iBossIndex = AddProfile(sProfile, SFF_FAKE); + if (iBossIndex != -1) + { + decl Float:eyePos[3], Float:eyeAng[3], Float:flPos[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, eyeAng, MASK_NPCSOLID, RayType_Infinite, TraceRayDontHitEntity, client); + TR_GetEndPosition(flPos, hTrace); + CloseHandle(hTrace); + + SpawnSlender(iBossIndex, flPos); + + LogAction(client, -1, "%N added a fake boss! (%s)", client, sProfile); + } + + return Plugin_Handled; +} + +public Action:Command_ForceState(client, args) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (args < 2) + { + ReplyToCommand(client, "Usage: sm_sf2_setplaystate <name|#userid> <0/1>"); + return Plugin_Handled; + } + + if (IsRoundEnding() || IsRoundInWarmup()) + { + CPrintToChat(client, "%t%T", "SF2 Prefix", "SF2 Cannot Use Command", client); + return Plugin_Handled; + } + + decl String:arg1[32]; + GetCmdArg(1, arg1, sizeof(arg1)); + + decl String:target_name[MAX_TARGET_LENGTH]; + decl target_list[MAXPLAYERS], target_count, bool:tn_is_ml; + + if ((target_count = ProcessTargetString( + arg1, + client, + target_list, + MAXPLAYERS, + 0, + target_name, + sizeof(target_name), + tn_is_ml)) <= 0) + { + ReplyToTargetError(client, target_count); + return Plugin_Handled; + } + + decl String:arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + + new iState = StringToInt(arg2); + + decl String:sName[MAX_NAME_LENGTH]; + + for (new i = 0; i < target_count; i++) + { + new target = target_list[i]; + GetClientName(target, sName, sizeof(sName)); + + if (iState && g_bPlayerEliminated[target]) + { + SetClientPlayState(target, true); + + CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced In Game", sName); + LogAction(client, target, "%N forced %N into the game.", client, target); + } + else if (!iState && !g_bPlayerEliminated[target]) + { + SetClientPlayState(target, false); + + CPrintToChatAll("%t %N: %t", "SF2 Prefix", client, "SF2 Player Forced Out Of Game", sName); + LogAction(client, target, "%N took %N out of the game.", client, target); + } + } + + return Plugin_Handled; +} + +public Action:Hook_CommandBuild(client, const String:command[], argc) +{ + if (!g_bEnabled) return Plugin_Continue; + if (!IsClientInPvP(client)) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Timer_BossCountUpdate(Handle:timer) +{ + if (timer != g_hBossCountUpdateTimer) return Plugin_Stop; + + if (!g_bEnabled) return Plugin_Stop; + + new iBossCount = NPCGetCount(); + new iBossPreferredCount; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1 || + g_iSlenderCopyMaster[i] != -1 || + (NPCGetFlags(i) & SFF_FAKE)) + { + continue; + } + + iBossPreferredCount++; + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsValidClient(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + IsClientInGhostMode(i) || + IsClientInDeathCam(i) || + DidClientEscape(i)) continue; + + // Check if we're near any bosses. + new iClosest = -1; + new Float:flBestDist = SF2_BOSS_PAGE_CALCULATION; + + for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) + { + if (NPCGetUniqueID(iBoss) == -1) continue; + if (NPCGetEntIndex(iBoss) == INVALID_ENT_REFERENCE) continue; + if (NPCGetFlags(iBoss) & SFF_FAKE) continue; + + new Float:flDist = NPCGetDistanceFromEntity(iBoss, i); + if (flDist < flBestDist) + { + iClosest = iBoss; + flBestDist = flDist; + break; + } + } + + if (iClosest != -1) continue; + + iClosest = -1; + flBestDist = SF2_BOSS_PAGE_CALCULATION; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient) || + !IsPlayerAlive(iClient) || + g_bPlayerEliminated[iClient] || + IsClientInGhostMode(iClient) || + IsClientInDeathCam(iClient) || + DidClientEscape(iClient)) + { + continue; + } + + new bool:bwub = false; + for (new iBoss = 0; iBoss < MAX_BOSSES; iBoss++) + { + if (NPCGetUniqueID(iBoss) == -1) continue; + if (NPCGetFlags(iBoss) & SFF_FAKE) continue; + + if (g_iSlenderTarget[iBoss] == iClient) + { + bwub = true; + break; + } + } + + if (!bwub) continue; + + new Float:flDist = EntityDistanceFromEntity(i, iClient); + if (flDist < flBestDist) + { + iClosest = iClient; + flBestDist = flDist; + } + } + + if (!IsValidClient(iClosest)) + { + // No one's close to this dude? DUDE! WE NEED ANOTHER BOSS! + iBossPreferredCount++; + } + } + + new iDiff = iBossCount - iBossPreferredCount; + if (iDiff) + { + if (iDiff > 0) + { + new iCount = iDiff; + // We need less bosses. Try and see if we can remove some. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_iSlenderCopyMaster[i] == -1) continue; + if (PeopleCanSeeSlender(i, _, false)) continue; + if (NPCGetFlags(i) & SFF_FAKE) continue; + + if (SlenderCanRemove(i)) + { + NPCRemove(i); + iCount--; + } + + if (iCount <= 0) + { + break; + } + } + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + new iCount = RoundToFloor(FloatAbs(float(iDiff))); + // Add new bosses (copy of the first boss). + for (new i = 0; i < MAX_BOSSES && iCount > 0; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + if (g_iSlenderCopyMaster[i] != -1) continue; + if (!(NPCGetFlags(i) & SFF_COPIES)) continue; + + // Get the number of copies I already have and see if I can have more copies. + new iCopyCount; + for (new i2 = 0; i2 < MAX_BOSSES; i2++) + { + if (NPCGetUniqueID(i2) == -1) continue; + if (g_iSlenderCopyMaster[i2] != i) continue; + + iCopyCount++; + } + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + if (iCopyCount >= GetProfileNum(sProfile, "copy_max", 10)) + { + continue; + } + + new iBossIndex = AddProfile(sProfile, _, i); + if (iBossIndex == -1) + { + LogError("Could not add copy for %d: No free slots!", i); + } + + iCount--; + } + } + } + + // Check if we can add some proxies. + if (!g_bRoundGrace) + { + if (NavMesh_Exists()) + { + new Handle:hProxyCandidates = CreateArray(); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) continue; + + if (g_iSlenderCopyMaster[iBossIndex] != -1) continue; // Copies cannot generate proxies. + + if (GetGameTime() < g_flSlenderTimeUntilNextProxy[iBossIndex]) continue; // Proxy spawning hasn't cooled down yet. + + new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); + if (!iTeleportTarget || iTeleportTarget == INVALID_ENT_REFERENCE) continue; // No teleport target. + + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumActiveProxies = 0; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) == iBossIndex) + { + iNumActiveProxies++; + } + } + + if (iNumActiveProxies >= iMaxProxies) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d has too many active proxies!", iBossIndex); +#endif + continue; + } + + new Float:flSpawnChanceMin = GetProfileFloat(sProfile, "proxies_spawn_chance_min"); + new Float:flSpawnChanceMax = GetProfileFloat(sProfile, "proxies_spawn_chance_max"); + new Float:flSpawnChanceThreshold = GetProfileFloat(sProfile, "proxies_spawn_chance_threshold") * NPCGetAnger(iBossIndex); + + new Float:flChance = GetRandomFloat(flSpawnChanceMin, flSpawnChanceMax); + if (flChance > flSpawnChanceThreshold) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's chances weren't in his favor!", iBossIndex); +#endif + continue; + } + + new iAvailableProxies = iMaxProxies - iNumActiveProxies; + + new iSpawnNumMin = GetProfileNum(sProfile, "proxies_spawn_num_min"); + new iSpawnNumMax = GetProfileNum(sProfile, "proxies_spawn_num_max"); + + new iSpawnNum = 0; + + // Get a list of people we can transform into a good Proxy. + ClearArray(hProxyCandidates); + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (g_bPlayerProxy[iClient]) continue; + + if (!g_iPlayerPreferences[iClient][PlayerPreference_EnableProxySelection]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your preferences.", iBossIndex); +#endif + continue; + } + + if (!g_bPlayerProxyAvailable[iClient]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because of your cooldown.", iBossIndex); +#endif + continue; + } + + if (g_bPlayerProxyAvailableInForce[iClient]) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're already being forced into a Proxy.", iBossIndex); +#endif + continue; + } + + if (!IsClientParticipating(iClient)) + { +#if defined DEBUG + SendDebugMessageToPlayer(iClient, DEBUG_BOSS_PROXIES, 0, "[PROXIES] You were rejected for being a proxy for boss %d because you're not participating.", iBossIndex); +#endif + continue; + } + + PushArrayCell(hProxyCandidates, iClient); + iSpawnNum++; + } + + if (iSpawnNum >= iSpawnNumMax) + { + iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNumMax); + } + else if (iSpawnNum >= iSpawnNumMin) + { + iSpawnNum = GetRandomInt(iSpawnNumMin, iSpawnNum); + } + + if (iSpawnNum <= 0) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d had a set spawn number of 0!", iBossIndex); +#endif + continue; + } + + decl Float:flTargetPos[3]; + GetClientAbsOrigin(iTeleportTarget, flTargetPos); + + new iTargetAreaIndex = NavMesh_GetNearestArea(flTargetPos); + if (iTargetAreaIndex == -1) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d's teleport target is not on the navmesh!", iBossIndex); +#endif + continue; // target is not on the nav mesh. + } + + // Search outwards until travel distance is at maximum range. + new Handle:hAreaArray = CreateArray(2); + new Handle:hAreas = CreateStack(); + NavMesh_CollectSurroundingAreas(hAreas, iTargetAreaIndex, g_flSlenderProxyTeleportMaxRange[iBossIndex]); + + new Float:flTeleportMinRange = CalculateTeleportMinRange(iBossIndex, g_flSlenderProxyTeleportMinRange[iBossIndex], g_flSlenderProxyTeleportMaxRange[iBossIndex]); + + { + new iAreaIndex = -1; + new iPoppedAreas = 0; + + while (!IsStackEmpty(hAreas)) + { + PopStackCell(hAreas, iAreaIndex); + new iCostSoFar = NavMeshArea_GetCostSoFar(iAreaIndex); + + if (float(iCostSoFar) >= flTeleportMinRange) + { + new iIndex = PushArrayCell(hAreaArray, iAreaIndex); + SetArrayCell(hAreaArray, iIndex, float(iCostSoFar), 1); + iPoppedAreas++; + } + } + + CloseHandle(hAreas); + + if (iPoppedAreas == 0) + { + // no areas to use! + CloseHandle(hAreaArray); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any sufficient surrounding areas!", iBossIndex); +#endif + + continue; + } +#if defined DEBUG + else + { + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d found %d surrounding areas", iBossIndex, iPoppedAreas); + } +#endif + } + + new Handle:hAreaArrayClose = CreateArray(); + new Handle:hAreaArrayAverage = CreateArray(); + new Handle:hAreaArrayFar = CreateArray(); + + for (new iRangeSection = 1; iRangeSection <= 3; iRangeSection++) + { + new Float:flRangeSectionMin = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection - 1) / 3.0); + new Float:flRangeSectionMax = flTeleportMinRange + (g_flSlenderProxyTeleportMaxRange[iBossIndex] - flTeleportMinRange) * (float(iRangeSection) / 3.0); + + for (new i = 0, iSize = GetArraySize(hAreaArray); i < iSize; i++) + { + new iAreaIndex = GetArrayCell(hAreaArray, i); + + decl Float:flAreaCenter[3]; + NavMeshArea_GetCenter(iAreaIndex, flAreaCenter); + + decl Float:flTestPos[3]; + decl Float:flEyeOffset[3]; + flEyeOffset[0] = 0.0; + flEyeOffset[1] = 0.0; + flEyeOffset[2] = HalfHumanHeight * 2.0; + + // Check visibility first. + if (IsPointVisibleToAPlayer(flAreaCenter, false, false)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (1)", iBossIndex, iAreaIndex); +#endif + continue; + } + + AddVectors(flAreaCenter, flEyeOffset, flTestPos); + + if (IsPointVisibleToAPlayer(flTestPos, false, false)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected visible area index %d! (2)", iBossIndex, iAreaIndex); +#endif + + continue; + } + + new iBoss = NPCGetEntIndex(iBossIndex); + + // Check space. First raise to HalfHumanHeight * 2, then trace downwards to get ground level. + { + decl Float:flTraceStartPos[3]; + flTraceStartPos[0] = flAreaCenter[0]; + flTraceStartPos[1] = flAreaCenter[1]; + flTraceStartPos[2] = flAreaCenter[2] + (HalfHumanHeight * 2.0); + + decl Float:flTraceMins[3]; + flTraceMins[0] = -20.0; + flTraceMins[1] = -20.0; + flTraceMins[2] = 0.0; + + decl Float:flTraceMaxs[3]; + flTraceMaxs[0] = 20.0; + flTraceMaxs[1] = 20.0; + flTraceMaxs[2] = 0.0; + + new Handle:hTrace = TR_TraceHullFilterEx(flTraceStartPos, + flAreaCenter, + flTraceMins, + flTraceMaxs, + MASK_NPCSOLID, + TraceRayDontHitEntity, + iBoss); + + decl Float:flTraceHitPos[3]; + TR_GetEndPosition(flTraceHitPos, hTrace); + flTraceHitPos[2] += 1.0; + CloseHandle(hTrace); + + static Float:flTraceSpaceMin[3] = { -20.0, -20.0, 0.0 }; + static Float:flTraceSpaceMax[3] = { 20.0, 20.0, 72.0 }; + + flTraceSpaceMax[2] = HalfHumanHeight * 2.0; + + if (IsSpaceOccupiedPlayer(flTraceHitPos, + flTraceSpaceMin, + flTraceSpaceMax, + iBoss == INVALID_ENT_REFERENCE ? -1 : iBoss)) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected too small area index %d! (2)", iBossIndex, iAreaIndex); +#endif + + continue; + } + } + + new bool:bTooNear = false; + + // Check minimum range. + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || + !IsPlayerAlive(iClient) || + g_bPlayerEliminated[iClient] || + DidClientEscape(iClient) || + g_bPlayerProxy[iClient] || + IsClientInGhostMode(iClient)) + { + continue; + } + + decl Float:flTempPos[3]; + GetClientAbsOrigin(iClient, flTempPos); + + if (GetVectorDistance(flAreaCenter, flTempPos) <= flTeleportMinRange) + { + bTooNear = true; + break; + } + } + + if (bTooNear) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d rejected near area index %d!", iBossIndex, iAreaIndex); +#endif + + continue; // This area is too close to a player. + } + + // Check travel distance. + new Float:flDist = Float:GetArrayCell(hAreaArray, i, 1); + if (flDist > flRangeSectionMin && flDist < flRangeSectionMax) + { + switch (iRangeSection) + { + case 1: PushArrayCell(hAreaArrayClose, iAreaIndex); + case 2: PushArrayCell(hAreaArrayAverage, iAreaIndex); + case 3: PushArrayCell(hAreaArrayFar, iAreaIndex); + } + } + } + } + + CloseHandle(hAreaArray); + + // Set the cooldown time! + new Float:flSpawnCooldownMin = GetProfileFloat(sProfile, "proxies_spawn_cooldown_min"); + new Float:flSpawnCooldownMax = GetProfileFloat(sProfile, "proxies_spawn_cooldown_max"); + + g_flSlenderTimeUntilNextProxy[iBossIndex] = GetGameTime() + GetRandomFloat(flSpawnCooldownMin, flSpawnCooldownMax); + + // Randomize the array. + SortADTArray(hProxyCandidates, Sort_Random, Sort_Integer); + + decl Float:flDestinationPos[3]; + + for (new iNum = 0; iNum < iSpawnNum && iNum < iAvailableProxies; iNum++) + { + new iClient = GetArrayCell(hProxyCandidates, iNum); + new iBestAreaIndex = -1; + + if (GetArraySize(hAreaArrayClose) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayClose, GetRandomInt(0, GetArraySize(hAreaArrayClose) - 1)); + } + else if (GetArraySize(hAreaArrayAverage) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayAverage, GetRandomInt(0, GetArraySize(hAreaArrayAverage) - 1)); + } + else if (GetArraySize(hAreaArrayFar) > 0) + { + iBestAreaIndex = GetArrayCell(hAreaArrayFar, GetRandomInt(0, GetArraySize(hAreaArrayFar) - 1)); + } + + if (iBestAreaIndex == -1) + { +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d could not find any areas to place proxies (spawned %d)!", iBossIndex, iNum); +#endif + break; + } + + NavMeshArea_GetCenter(iBestAreaIndex, flDestinationPos); + + if (!GetConVarBool(g_cvPlayerProxyAsk)) + { + ClientStartProxyForce(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); + } + else + { + DisplayProxyAskMenu(iClient, NPCGetUniqueID(iBossIndex), flDestinationPos); + } + } + + CloseHandle(hAreaArrayClose); + CloseHandle(hAreaArrayAverage); + CloseHandle(hAreaArrayFar); + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_PROXIES, 0, "[PROXIES] Boss %d finished proxy process!", iBossIndex); +#endif + } + + CloseHandle(hProxyCandidates); + } + } + + return Plugin_Continue; +} + +ReloadRestrictedWeapons() +{ + if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) + { + CloseHandle(g_hRestrictedWeaponsConfig); + g_hRestrictedWeaponsConfig = INVALID_HANDLE; + } + + decl String:buffer[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, buffer, sizeof(buffer), FILE_RESTRICTEDWEAPONS); + new Handle:kv = CreateKeyValues("root"); + if (!FileToKeyValues(kv, buffer)) + { + CloseHandle(kv); + LogError("Failed to load restricted weapons list! File not found!"); + } + else + { + g_hRestrictedWeaponsConfig = kv; + LogSF2Message("Reloaded restricted weapons configuration file successfully"); + } +} + +public Action:Timer_RoundMessages(Handle:timer) +{ + if (!g_bEnabled) return Plugin_Stop; + + if (timer != g_hRoundMessagesTimer) return Plugin_Stop; + + switch (g_iRoundMessagesNum) + { + case 0: CPrintToChatAll("{olive}==== {lightgreen}Slender Fortress (%s){olive} coded by {lightgreen}Kit o' Rifty{olive} ====", PLUGIN_VERSION_DISPLAY); + case 1: CPrintToChatAll("%t", "SF2 Ad Message 1"); + case 2: CPrintToChatAll("%t", "SF2 Ad Message 2"); + } + + g_iRoundMessagesNum++; + if (g_iRoundMessagesNum > 2) g_iRoundMessagesNum = 0; + + return Plugin_Continue; +} + +public Action:Timer_WelcomeMessage(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + CPrintToChat(client, "%T", "SF2 Welcome Message", client); +} + +GetMaxPlayersForRound() +{ + new iOverride = GetConVarInt(g_cvMaxPlayersOverride); + if (iOverride != -1) return iOverride; + return GetConVarInt(g_cvMaxPlayers); +} + +public OnConVarChanged(Handle:cvar, const String:oldValue[], const String:newValue[]) +{ + if (cvar == g_cvDifficulty) + { + switch (StringToInt(newValue)) + { + case Difficulty_Easy: g_flRoundDifficultyModifier = DIFFICULTY_EASY; + case Difficulty_Hard: g_flRoundDifficultyModifier = DIFFICULTY_HARD; + case Difficulty_Insane: g_flRoundDifficultyModifier = DIFFICULTY_INSANE; + default: g_flRoundDifficultyModifier = DIFFICULTY_NORMAL; + } + } + else if (cvar == g_cvMaxPlayers || cvar == g_cvMaxPlayersOverride) + { + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + CheckPlayerGroup(i); + } + } + else if (cvar == g_cvPlayerShakeEnabled) + { + g_bPlayerShakeEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobEnabled) + { + g_bPlayerViewbobEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobHurtEnabled) + { + g_bPlayerViewbobHurtEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvPlayerViewbobSprintEnabled) + { + g_bPlayerViewbobSprintEnabled = bool:StringToInt(newValue); + } + else if (cvar == g_cvGravity) + { + g_flGravity = StringToFloat(newValue); + } + else if (cvar == g_cv20Dollars) + { + g_b20Dollars = bool:StringToInt(newValue); + } + else if (cvar == g_cvAllChat) + { + if (g_bEnabled) + { + for (new i = 1; i <= MaxClients; i++) + { + ClientUpdateListeningFlags(i); + } + } + } +} + +// ========================================================== +// IN-GAME AND ENTITY HOOK FUNCTIONS +// ========================================================== + + +public OnEntityCreated(ent, const String:classname[]) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(ent) || ent <= 0) return; + + if (StrEqual(classname, "spotlight_end", false)) + { + SDKHook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); + } + else if (StrEqual(classname, "beam", false)) + { + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightBeamSetTransmit); + } + + PvP_OnEntityCreated(ent, classname); +} + +public OnEntityDestroyed(ent) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(ent) || ent <= 0) return; + + decl String:sClassname[64]; + GetEntityClassname(ent, sClassname, sizeof(sClassname)); + + if (StrEqual(sClassname, "light_dynamic", false)) + { + AcceptEntityInput(ent, "TurnOff"); + + new iEnd = INVALID_ENT_REFERENCE; + while ((iEnd = FindEntityByClassname(iEnd, "spotlight_end")) != -1) + { + if (GetEntPropEnt(iEnd, Prop_Data, "m_hOwnerEntity") == ent) + { + AcceptEntityInput(iEnd, "Kill"); + break; + } + } + } + + PvP_OnEntityDestroyed(ent, sClassname); +} + +public Action:Hook_BlockUserMessage(UserMsg:msg_id, Handle:bf, const players[], playersNum, bool:reliable, bool:init) +{ + if (!g_bEnabled) return Plugin_Continue; + return Plugin_Handled; +} + +public Action:Hook_NormalSound(clients[64], &numClients, String:sample[PLATFORM_MAX_PATH], &entity, &channel, &Float:volume, &level, &pitch, &flags) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsValidClient(entity)) + { + if (IsClientInGhostMode(entity)) + { + switch (channel) + { + case SNDCHAN_VOICE, SNDCHAN_WEAPON, SNDCHAN_ITEM, SNDCHAN_BODY: return Plugin_Handled; + } + } + else if (g_bPlayerProxy[entity]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[entity]); + if (iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + switch (channel) + { + case SNDCHAN_VOICE: + { + if (!bool:GetProfileNum(sProfile, "proxies_allownormalvoices", 1)) + { + return Plugin_Handled; + } + } + } + } + } + else if (!g_bPlayerEliminated[entity]) + { + switch (channel) + { + case SNDCHAN_VOICE: + { + if (IsRoundInIntro()) return Plugin_Handled; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Voice)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDVOICE; + } + } + } + case SNDCHAN_BODY: + { + if (!StrContains(sample, "player/footsteps", false) || StrContains(sample, "step", false) != -1) + { + if (GetConVarBool(g_cvPlayerViewbobSprintEnabled) && IsClientReallySprinting(entity)) + { + // Viewpunch. + new Float:flPunchVelStep[3]; + + decl Float:flVelocity[3]; + GetEntPropVector(entity, Prop_Data, "m_vecAbsVelocity", flVelocity); + new Float:flSpeed = GetVectorLength(flVelocity); + + flPunchVelStep[0] = flSpeed / 300.0; + flPunchVelStep[1] = 0.0; + flPunchVelStep[2] = 0.0; + + ClientViewPunch(entity, flPunchVelStep); + } + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Footstep)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEP; + + if (IsClientSprinting(entity) && !(GetEntProp(entity, Prop_Send, "m_bDucking") || GetEntProp(entity, Prop_Send, "m_bDucked"))) + { + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDFOOTSTEPLOUD; + } + } + } + } + } + case SNDCHAN_ITEM, SNDCHAN_WEAPON: + { + if (StrContains(sample, "impact", false) != -1 || StrContains(sample, "hit", false) != -1) + { + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + if (SlenderCanHearPlayer(iBossIndex, entity, SoundType_Weapon)) + { + GetClientAbsOrigin(entity, g_flSlenderTargetSoundTempPos[iBossIndex]); + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDSUSPICIOUSSOUND; + g_iSlenderInterruptConditions[iBossIndex] |= COND_HEARDWEAPON; + } + } + } + } + } + } + } + + new bool:bModified = false; + + for (new i = 0; i < numClients; i++) + { + new iClient = clients[i]; + if (IsValidClient(iClient) && IsPlayerAlive(iClient) && !IsClientInGhostMode(iClient)) + { + new bool:bCanHearSound = true; + + if (IsValidClient(entity) && entity != iClient) + { + if (!g_bPlayerEliminated[iClient]) + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (!g_bPlayerEliminated[entity] && !DidClientEscape(entity)) + { + bCanHearSound = false; + } + } + } + } + + if (!bCanHearSound) + { + bModified = true; + clients[i] = -1; + } + } + } + + if (bModified) return Plugin_Changed; + return Plugin_Continue; +} + +public MRESReturn:Hook_EntityShouldTransmit(thisPointer, Handle:hReturn, Handle:hParams) +{ + if (!g_bEnabled) return MRES_Ignored; + + if (IsValidClient(thisPointer)) + { + if (DoesClientHaveConstantGlow(thisPointer)) + { + DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. + return MRES_Supercede; + } + } + else + { + new iBossIndex = NPCGetFromEntIndex(thisPointer); + if (iBossIndex != -1) + { + DHookSetReturn(hReturn, FL_EDICT_ALWAYS); // Should always transmit, but our SetTransmit hook gets the final say. + return MRES_Supercede; + } + } + + return MRES_Ignored; +} + +public Hook_TriggerOnStartTouch(const String:output[], caller, activator, Float:delay) +{ + if (!g_bEnabled) return; + + if (!IsValidEntity(caller)) return; + + decl String:sName[64]; + GetEntPropString(caller, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrContains(sName, "sf2_escape_trigger", false) == 0) + { + if (IsRoundInEscapeObjective()) + { + if (IsValidClient(activator) && IsPlayerAlive(activator) && !IsClientInDeathCam(activator) && !g_bPlayerEliminated[activator] && !DidClientEscape(activator)) + { + ClientEscape(activator); + TeleportClientToEscapePoint(activator); + } + } + } + + PvP_OnTriggerStartTouch(caller, activator); +} + +public Hook_TriggerOnEndTouch(const String:sOutput[], caller, activator, Float:flDelay) +{ + if (!g_bEnabled) return; + + PvP_OnTriggerEndTouch(caller, activator); +} + +public Action:Hook_PageOnTakeDamage(page, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsValidClient(attacker)) + { + if (!g_bPlayerEliminated[attacker]) + { + if (damagetype & 0x80) // 0x80 == melee damage + { + CollectPage(page, attacker); + } + } + } + + return Plugin_Continue; +} + +static CollectPage(page, activator) +{ + SetPageCount(g_iPageCount + 1); + g_iPlayerPageCount[activator] += 1; + EmitSoundToAll(PAGE_GRABSOUND, activator, SNDCHAN_ITEM, SNDLEVEL_SCREAMING); + + // Gives points. Credit to the makers of VSH/FF2. + new Handle:hEvent = CreateEvent("player_escort_score", true); + SetEventInt(hEvent, "player", activator); + SetEventInt(hEvent, "points", 1); + FireEvent(hEvent); + + AcceptEntityInput(page, "FireUser1"); + AcceptEntityInput(page, "Kill"); +} + +// ========================================================== +// GENERIC CLIENT HOOKS AND FUNCTIONS +// ========================================================== + + +public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2]) +{ + if (!g_bEnabled) return Plugin_Continue; + + ClientDisableFakeLagCompensation(client); + + // Check impulse (block spraying and built-in flashlight) + switch (impulse) + { + case 100: + { + impulse = 0; + } + case 201: + { + if (IsClientInGhostMode(client)) + { + impulse = 0; + } + } + } + + for (new i = 0; i < MAX_BUTTONS; i++) + { + new button = (1 << i); + + if ((buttons & button)) + { + if (!(g_iPlayerLastButtons[client] & button)) + { + ClientOnButtonPress(client, button); + } + } + else if ((g_iPlayerLastButtons[client] & button)) + { + ClientOnButtonRelease(client, button); + } + } + + g_iPlayerLastButtons[client] = buttons; + + return Plugin_Continue; +} + + +public OnClientCookiesCached(client) +{ + if (!g_bEnabled) return; + + // Load our saved settings. + new String:sCookie[64]; + GetClientCookie(client, g_hCookie, sCookie, sizeof(sCookie)); + + g_iPlayerQueuePoints[client] = 0; + + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; + g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = true; + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = true; + + if (sCookie[0]) + { + new String:s2[12][32]; + new count = ExplodeString(sCookie, " ; ", s2, 12, 32); + + if (count > 0) + g_iPlayerQueuePoints[client] = StringToInt(s2[0]); + if (count > 1) + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = bool:StringToInt(s2[1]); + if (count > 2) + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode:StringToInt(s2[2]); + if (count > 3) + g_iPlayerPreferences[client][PlayerPreference_FilmGrain] = bool:StringToInt(s2[3]); + if (count > 4) + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = bool:StringToInt(s2[4]); + if (count > 5) + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay] = bool:StringToInt(s2[5]); + } +} + +public OnClientPutInServer(client) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientPutInServer(%d)", client); +#endif + + ClientSetPlayerGroup(client, -1); + + g_bPlayerEscaped[client] = false; + g_bPlayerEliminated[client] = true; + g_bPlayerChoseTeam[client] = false; + g_bPlayerPlayedSpecialRound[client] = true; + g_bPlayerPlayedNewBossRound[client] = true; + + g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn] = false; + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; + + g_iPlayerPageCount[client] = 0; + g_iPlayerDesiredFOV[client] = 90; + + SDKHook(client, SDKHook_PreThink, Hook_ClientPreThink); + SDKHook(client, SDKHook_SetTransmit, Hook_ClientSetTransmit); + SDKHook(client, SDKHook_OnTakeDamage, Hook_ClientOnTakeDamage); + + DHookEntity(g_hSDKWantsLagCompensationOnEntity, true, client); + DHookEntity(g_hSDKShouldTransmit, true, client); + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + + SetPlayerGroupInvitedPlayer(i, client, false); + SetPlayerGroupInvitedPlayerCount(i, client, 0); + SetPlayerGroupInvitedPlayerTime(i, client, 0.0); + } + + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientUpdateListeningFlags(client); + ClientUpdateMusicSystem(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + ClientResetProxy(client); + ClientResetHints(client); + ClientResetScare(client); + + ClientResetDeathCam(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientSetScareBoostEndTime(client, -1.0); + + ClientStartProxyAvailableTimer(client); + + if (!IsFakeClient(client)) + { + // See if the player is using the projected flashlight. + QueryClientConVar(client, "mat_supportflashlight", OnClientGetProjectedFlashlightSetting); + + // Get desired FOV. + QueryClientConVar(client, "fov_desired", OnClientGetDesiredFOV); + } + + PvP_OnClientPutInServer(client); + +#if defined DEBUG + g_iPlayerDebugFlags[client] = 0; + + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientPutInServer(%d)", client); +#endif +} + +public OnClientGetProjectedFlashlightSetting(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) +{ + if (result != ConVarQuery_Okay) + { + LogError("Warning: Player %N failed to query for ConVar mat_supportflashlight", client); + return; + } + + if (StringToInt(cvarValue)) + { + decl String:sAuth[64]; + GetClientAuthString(client, sAuth, sizeof(sAuth)); + + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = true; + LogSF2Message("Player %N (%s) has mat_supportflashlight enabled, projected flashlight will be used", client, sAuth); + } +} + +public OnClientGetDesiredFOV(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) +{ + if (!IsValidClient(client)) return; + + g_iPlayerDesiredFOV[client] = StringToInt(cvarValue); +} + +public OnClientDisconnect(client) +{ + DestroySpriteOverlay(client); + g_hOverlayUpdateTimer[client] = INVALID_HANDLE; + + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START OnClientDisconnect(%d)", client); +#endif + + g_bPlayerEscaped[client] = false; + + // Save and reset settings for the next client. + ClientSaveCookies(client); + ClientSetPlayerGroup(client, -1); + + // Reset variables. + g_iPlayerPreferences[client][PlayerPreference_ShowHints] = true; + g_iPlayerPreferences[client][PlayerPreference_MuteMode] = MuteMode_Normal; + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection] = true; + g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; + + // Reset any client functions that may be still active. + ClientResetOverlay(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientSetGhostModeState(client, false); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientStopProxyForce(client); + + if (!IsRoundInWarmup()) + { + if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) + { + if (g_bRoundGrace) + { + // Force the next player in queue to take my place, if any. + ForceInNextPlayersInQueue(1, true); + } + else + { + if (!IsRoundEnding()) + { + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + } + + // Reset queue points global variable. + g_iPlayerQueuePoints[client] = 0; + + PvP_OnClientDisconnect(client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END OnClientDisconnect(%d)", client); +#endif +} + +public OnClientDisconnect_Post(client) +{ + g_iPlayerLastButtons[client] = 0; +} + +public TF2_OnWaitingForPlayersStart() +{ + g_bRoundWaitingForPlayers = true; +} + +public TF2_OnWaitingForPlayersEnd() +{ + g_bRoundWaitingForPlayers = false; +} + +SF2RoundState:GetRoundState() +{ + return g_iRoundState; +} + +SetRoundState(SF2RoundState:iRoundState) +{ + if (g_iRoundState == iRoundState) return; + + PrintToServer("SetRoundState(%d)", iRoundState); + + new SF2RoundState:iOldRoundState = GetRoundState(); + g_iRoundState = iRoundState; + + // Cleanup from old roundstate if needed. + switch (iOldRoundState) + { + case SF2RoundState_Waiting: + { + } + case SF2RoundState_Intro: + { + g_hRoundIntroTimer = INVALID_HANDLE; + } + case SF2RoundState_Active: + { + g_bRoundGrace = false; + g_hRoundGraceTimer = INVALID_HANDLE; + g_hRoundTimer = INVALID_HANDLE; + } + case SF2RoundState_Escape: + { + g_hRoundTimer = INVALID_HANDLE; + } + case SF2RoundState_Outro: + { + } + } + + switch (g_iRoundState) + { + case SF2RoundState_Waiting: + { + } + case SF2RoundState_Intro: + { + g_hRoundIntroTimer = INVALID_HANDLE; + g_iRoundIntroText = 0; + g_bRoundIntroTextDefault = false; + g_hRoundIntroTextTimer = CreateTimer(0.0, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hRoundIntroTextTimer); + + // Gather data on the intro parameters set by the map. + new Float:flHoldTime = g_flRoundIntroFadeHoldTime; + g_hRoundIntroTimer = CreateTimer(flHoldTime, Timer_ActivateRoundFromIntro, _, TIMER_FLAG_NO_MAPCHANGE); + + // Trigger any intro logic entities, if any. + new ent = -1; + while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) + { + decl String:sName[64]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_intro_relay", false)) + { + AcceptEntityInput(ent, "Trigger"); + break; + } + } + } + case SF2RoundState_Active: + { + // Start the grace period timer. + g_bRoundGrace = true; + g_hRoundGraceTimer = CreateTimer(GetConVarFloat(g_cvGraceTime), Timer_RoundGrace, _, TIMER_FLAG_NO_MAPCHANGE); + + CreateTimer(2.0, Timer_RoundStart, _, TIMER_FLAG_NO_MAPCHANGE); + + // Enable movement on players. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + SetEntityFlags(i, GetEntityFlags(i) & ~FL_FROZEN); + } + + // Fade in. + new Float:flFadeTime = g_flRoundIntroFadeDuration; + new iFadeFlags = SF_FADE_IN | FFADE_PURGE; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + UTIL_ScreenFade(i, FixedUnsigned16(flFadeTime, 1 << 12), 0, iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); + } + } + case SF2RoundState_Escape: + { + // Initialize the escape timer, if needed. + if (g_iRoundEscapeTimeLimit > 0) + { + g_iRoundTime = g_iRoundEscapeTimeLimit; + g_hRoundTimer = CreateTimer(1.0, Timer_RoundTimeEscape, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hRoundTimer = INVALID_HANDLE; + } + + decl String:sName[32]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_logic_escape", false)) + { + AcceptEntityInput(ent, "FireUser1"); + break; + } + } + } + case SF2RoundState_Outro: + { + if (!g_bRoundHasEscapeObjective) + { + // Teleport winning players to the escape point. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + TeleportClientToEscapePoint(i); + } + } + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (IsClientInGhostMode(i)) + { + // Take the player out of ghost mode. + ClientSetGhostModeState(i, false); + TF2_RespawnPlayer(i); + } + else if (g_bPlayerProxy[i]) + { + TF2_RespawnPlayer(i); + } + + if (!g_bPlayerEliminated[i]) + { + // Give them back all their weapons so they can beat the crap out of the other team. + TF2_RegeneratePlayer(i); + } + + ClientUpdateListeningFlags(i); + } + } + } +} + +bool:IsRoundInEscapeObjective() +{ + return bool:(GetRoundState() == SF2RoundState_Escape); +} + +bool:IsRoundInWarmup() +{ + return bool:(GetRoundState() == SF2RoundState_Waiting); +} + +bool:IsRoundInIntro() +{ + return bool:(GetRoundState() == SF2RoundState_Intro); +} + +bool:IsRoundEnding() +{ + return bool:(GetRoundState() == SF2RoundState_Outro); +} + +bool:IsInfiniteBlinkEnabled() +{ + return bool:(g_bRoundInfiniteBlink || (GetConVarInt(g_cvPlayerInfiniteBlinkOverride) == 1)); +} + +bool:IsInfiniteFlashlightEnabled() +{ + return bool:(g_bRoundInfiniteFlashlight || (GetConVarInt(g_cvPlayerInfiniteFlashlightOverride) == 1)); +} + +bool:IsInfiniteSprintEnabled() +{ + return bool:(g_bRoundInfiniteSprint || (GetConVarInt(g_cvPlayerInfiniteSprintOverride) == 1)); +} + + +#define SF2_PLAYER_HUD_BLINK_SYMBOL "B" +#define SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL "ÏŸ" +#define SF2_PLAYER_HUD_BAR_SYMBOL "|" +#define SF2_PLAYER_HUD_BAR_MISSING_SYMBOL "" +#define SF2_PLAYER_HUD_INFINITY_SYMBOL "∞" +#define SF2_PLAYER_HUD_SPRINT_SYMBOL "»" + +public Action:Timer_ClientAverageUpdate(Handle:timer) +{ + if (timer != g_hClientAverageUpdateTimer) return Plugin_Stop; + + if (!g_bEnabled) return Plugin_Stop; + + if (IsRoundInWarmup() || IsRoundEnding()) return Plugin_Continue; + + // First, process through HUD stuff. + decl String:buffer[256]; + + static iHudColorHealthy[3] = { 150, 255, 150 }; + static iHudColorCritical[3] = { 255, 10, 10 }; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (IsPlayerAlive(i) && !IsClientInDeathCam(i)) + { + if (!g_bPlayerEliminated[i]) + { + if (DidClientEscape(i)) continue; + + new iMaxBars = 12; + new iBars = RoundToCeil(float(iMaxBars) * ClientGetBlinkMeter(i)); + if (iBars > iMaxBars) iBars = iMaxBars; + + Format(buffer, sizeof(buffer), "%s ", SF2_PLAYER_HUD_BLINK_SYMBOL); + + if (IsInfiniteBlinkEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + + if (!g_bSpecialRound || g_iSpecialRoundType != SPECIALROUND_LIGHTSOUT) + { + iBars = RoundToCeil(float(iMaxBars) * ClientGetFlashlightBatteryLife(i)); + if (iBars > iMaxBars) iBars = iMaxBars; + + decl String:sBuffer2[64]; + Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_FLASHLIGHT_SYMBOL); + StrCat(buffer, sizeof(buffer), sBuffer2); + + if (IsInfiniteFlashlightEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + } + + iBars = RoundToCeil(float(iMaxBars) * (float(ClientGetSprintPoints(i)) / 100.0)); + if (iBars > iMaxBars) iBars = iMaxBars; + + decl String:sBuffer2[64]; + Format(sBuffer2, sizeof(sBuffer2), "\n%s ", SF2_PLAYER_HUD_SPRINT_SYMBOL); + StrCat(buffer, sizeof(buffer), sBuffer2); + + if (IsInfiniteSprintEnabled()) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_INFINITY_SYMBOL); + } + else + { + for (new i2 = 0; i2 < iMaxBars; i2++) + { + if (i2 < iBars) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + else + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_MISSING_SYMBOL); + } + } + } + + + new Float:flHealthRatio = float(GetEntProp(i, Prop_Send, "m_iHealth")) / float(SDKCall(g_hSDKGetMaxHealth, i)); + + new iColor[3]; + for (new i2 = 0; i2 < 3; i2++) + { + iColor[i2] = RoundFloat(float(iHudColorHealthy[i2]) + (float(iHudColorCritical[i2] - iHudColorHealthy[i2]) * (1.0 - flHealthRatio))); + } + + SetHudTextParams(0.035, 0.83, + 0.3, + iColor[0], + iColor[1], + iColor[2], + 40, + _, + 1.0, + 0.07, + 0.5); + ShowSyncHudText(i, g_hHudSync2, buffer); + } + else + { + if (g_bPlayerProxy[i]) + { + new iMaxBars = 12; + new iBars = RoundToCeil(float(iMaxBars) * (float(g_iPlayerProxyControl[i]) / 100.0)); + if (iBars > iMaxBars) iBars = iMaxBars; + + strcopy(buffer, sizeof(buffer), "CONTROL\n"); + + for (new i2 = 0; i2 < iBars; i2++) + { + StrCat(buffer, sizeof(buffer), SF2_PLAYER_HUD_BAR_SYMBOL); + } + + SetHudTextParams(-1.0, 0.83, + 0.3, + SF2_HUD_TEXT_COLOR_R, + SF2_HUD_TEXT_COLOR_G, + SF2_HUD_TEXT_COLOR_B, + 40, + _, + 1.0, + 0.07, + 0.5); + ShowSyncHudText(i, g_hHudSync2, buffer); + } + } + } + + ClientUpdateListeningFlags(i); + ClientUpdateMusicSystem(i); + } + + return Plugin_Continue; +} + +stock bool:IsClientParticipating(client) +{ + if (!IsValidClient(client)) return false; + + if (bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) + { + // Who would coach in this game? + return false; + } + + new iTeam = GetClientTeam(client); + + if (g_bPlayerLagCompensation[client]) + { + iTeam = g_iPlayerLagCompensationTeam[client]; + } + + switch (iTeam) + { + case TFTeam_Unassigned, TFTeam_Spectator: return false; + } + + if (_:TF2_GetPlayerClass(client) == 0) + { + // Player hasn't chosen a class? What. + return false; + } + + return true; +} + +Handle:GetQueueList() +{ + new Handle:hArray = CreateArray(3); + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientParticipating(i)) continue; + if (IsPlayerGroupActive(ClientGetPlayerGroup(i))) continue; + + new index = PushArrayCell(hArray, i); + SetArrayCell(hArray, index, g_iPlayerQueuePoints[i], 1); + SetArrayCell(hArray, index, false, 2); + } + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + new index = PushArrayCell(hArray, i); + SetArrayCell(hArray, index, GetPlayerGroupQueuePoints(i), 1); + SetArrayCell(hArray, index, true, 2); + } + + if (GetArraySize(hArray)) SortADTArrayCustom(hArray, SortQueueList); + return hArray; +} + +SetClientPlayState(client, bool:bState, bool:bEnablePlay=true) +{ + if (bState) + { + if (!g_bPlayerEliminated[client]) return; + + g_bPlayerEliminated[client] = false; + g_bPlayerPlaying[client] = bEnablePlay; + g_hPlayerSwitchBlueTimer[client] = INVALID_HANDLE; + + ClientSetGhostModeState(client, false); + + PvP_SetPlayerPvPState(client, false, false, false); + + if (g_bSpecialRound) + { + SetClientPlaySpecialRoundState(client, true); + } + + if (g_bNewBossRound) + { + SetClientPlayNewBossRoundState(client, true); + } + + if (TF2_GetPlayerClass(client) == TFClassType:0) + { + // Player hasn't chosen a class for some reason. Choose one for him. + TF2_SetPlayerClass(client, TFClassType:GetRandomInt(1, 9), true, true); + } + + ChangeClientTeamNoSuicide(client, _:TFTeam_Red); + } + else + { + if (g_bPlayerEliminated[client]) return; + + g_bPlayerEliminated[client] = true; + g_bPlayerPlaying[client] = false; + + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + } +} + +bool:DidClientPlayNewBossRound(client) +{ + return g_bPlayerPlayedNewBossRound[client]; +} + +SetClientPlayNewBossRoundState(client, bool:bState) +{ + g_bPlayerPlayedNewBossRound[client] = bState; +} + +bool:DidClientPlaySpecialRound(client) +{ + return g_bPlayerPlayedNewBossRound[client]; +} + +SetClientPlaySpecialRoundState(client, bool:bState) +{ + g_bPlayerPlayedSpecialRound[client] = bState; +} + +TeleportClientToEscapePoint(client) +{ + if (!IsClientInGame(client)) return; + + new ent = EntRefToEntIndex(g_iRoundEscapePointEntity); + if (ent && ent != -1) + { + decl Float:flPos[3], Float:flAng[3]; + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", flPos); + GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", flAng); + + TeleportEntity(client, flPos, flAng, Float:{ 0.0, 0.0, 0.0 }); + AcceptEntityInput(ent, "FireUser1", client); + } +} + +ForceInNextPlayersInQueue(iAmount, bool:bShowMessage=false) +{ + // Grab the next person in line, or the next group in line if space allows. + new iAmountLeft = iAmount; + new Handle:hPlayers = CreateArray(); + new Handle:hArray = GetQueueList(); + + for (new i = 0, iSize = GetArraySize(hArray); i < iSize && iAmountLeft > 0; i++) + { + if (!GetArrayCell(hArray, i, 2)) + { + new iClient = GetArrayCell(hArray, i); + if (g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; + + PushArrayCell(hPlayers, iClient); + iAmountLeft--; + } + else + { + new iGroupIndex = GetArrayCell(hArray, i); + if (!IsPlayerGroupActive(iGroupIndex)) continue; + + new iMemberCount = GetPlayerGroupMemberCount(iGroupIndex); + if (iMemberCount <= iAmountLeft) + { + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient) || g_bPlayerPlaying[iClient] || !g_bPlayerEliminated[iClient] || !IsClientParticipating(iClient)) continue; + if (ClientGetPlayerGroup(iClient) == iGroupIndex) + { + PushArrayCell(hPlayers, iClient); + } + } + + SetPlayerGroupPlaying(iGroupIndex, true); + + iAmountLeft -= iMemberCount; + } + } + } + + CloseHandle(hArray); + + for (new i = 0, iSize = GetArraySize(hPlayers); i < iSize; i++) + { + new iClient = GetArrayCell(hPlayers, i); + ClientSetQueuePoints(iClient, 0); + SetClientPlayState(iClient, true); + + if (bShowMessage) CPrintToChat(iClient, "%T", "SF2 Force Play", iClient); + } + + CloseHandle(hPlayers); +} + +public SortQueueList(index1, index2, Handle:array, Handle:hndl) +{ + new iQueuePoints1 = GetArrayCell(array, index1, 1); + new iQueuePoints2 = GetArrayCell(array, index2, 1); + + if (iQueuePoints1 > iQueuePoints2) return -1; + else if (iQueuePoints1 == iQueuePoints2) return 0; + return 1; +} + +// ========================================================== +// GENERIC PAGE/BOSS HOOKS AND FUNCTIONS +// ========================================================== + +public Action:Hook_SlenderObjectSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!IsPlayerAlive(other) || IsClientInDeathCam(other)) + { + if (!IsValidEdict(GetEntPropEnt(other, Prop_Send, "m_hObserverTarget"))) return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action:Timer_SlenderBlinkBossThink(Handle:timer, any:entref) +{ + new slender = EntRefToEntIndex(entref); + if (!slender || slender == INVALID_ENT_REFERENCE) return Plugin_Stop; + + new iBossIndex = NPCGetFromEntIndex(slender); + if (iBossIndex == -1) return Plugin_Stop; + + if (timer != g_hSlenderEntityThink[iBossIndex]) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (NPCGetType(iBossIndex) == SF2BossType_Creeper) + { + new bool:bMove = false; + + if ((GetGameTime() - g_flSlenderLastKill[iBossIndex]) >= GetProfileFloat(sProfile, "kill_cooldown")) + { + if (PeopleCanSeeSlender(iBossIndex, false, false) && !PeopleCanSeeSlender(iBossIndex, true, SlenderUsesBlink(iBossIndex))) + { + new iBestPlayer = -1; + new Handle:hArray = CreateArray(); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsClientInDeathCam(i) || g_bPlayerEliminated[i] || DidClientEscape(i) || IsClientInGhostMode(i) || !PlayerCanSeeSlender(i, iBossIndex, false, false)) continue; + PushArrayCell(hArray, i); + } + + if (GetArraySize(hArray)) + { + decl Float:flSlenderPos[3]; + SlenderGetAbsOrigin(iBossIndex, flSlenderPos); + + decl Float:flTempPos[3]; + new iTempPlayer = -1; + new Float:flTempDist = 16384.0; + for (new i = 0; i < GetArraySize(hArray); i++) + { + new iClient = GetArrayCell(hArray, i); + GetClientAbsOrigin(iClient, flTempPos); + if (GetVectorDistance(flTempPos, flSlenderPos) < flTempDist) + { + iTempPlayer = iClient; + flTempDist = GetVectorDistance(flTempPos, flSlenderPos); + } + } + + iBestPlayer = iTempPlayer; + } + + CloseHandle(hArray); + + decl Float:buffer[3]; + if (iBestPlayer != -1 && SlenderCalculateApproachToPlayer(iBossIndex, iBestPlayer, buffer)) + { + bMove = true; + + decl Float:flAng[3], Float:flBuffer[3]; + decl Float:flSlenderPos[3], Float:flPos[3]; + GetEntPropVector(slender, Prop_Data, "m_vecAbsOrigin", flSlenderPos); + GetClientAbsOrigin(iBestPlayer, flPos); + SubtractVectors(flPos, buffer, flAng); + GetVectorAngles(flAng, flAng); + + // Take care of angle offsets. + AddVectors(flAng, g_flSlenderEyeAngOffset[iBossIndex], flAng); + for (new i = 0; i < 3; i++) flAng[i] = AngleNormalize(flAng[i]); + + flAng[0] = 0.0; + + // Take care of position offsets. + GetProfileVector(sProfile, "pos_offset", flBuffer); + AddVectors(buffer, flBuffer, buffer); + + TeleportEntity(slender, buffer, flAng, NULL_VECTOR); + + new Float:flMaxRange = GetProfileFloat(sProfile, "teleport_range_max"); + new Float:flDist = GetVectorDistance(buffer, flPos); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + + if (flDist < (flMaxRange * 0.33)) + { + GetProfileString(sProfile, "model_closedist", sBuffer, sizeof(sBuffer)); + } + else if (flDist < (flMaxRange * 0.66)) + { + GetProfileString(sProfile, "model_averagedist", sBuffer, sizeof(sBuffer)); + } + else + { + GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); + } + + // Fallback if error. + if (!sBuffer[0]) GetProfileString(sProfile, "model", sBuffer, sizeof(sBuffer)); + + SetEntProp(slender, Prop_Send, "m_nModelIndex", PrecacheModel(sBuffer)); + + if (flDist <= NPCGetInstantKillRadius(iBossIndex)) + { + if (NPCGetFlags(iBossIndex) & SFF_FAKE) + { + SlenderMarkAsFake(iBossIndex); + return Plugin_Stop; + } + else + { + g_flSlenderLastKill[iBossIndex] = GetGameTime(); + ClientStartDeathCam(iBestPlayer, iBossIndex, buffer); + } + } + } + } + } + + if (bMove) + { + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_move_single", sBuffer, sizeof(sBuffer)); + if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + + GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); + if (sBuffer[0]) EmitSoundToAll(sBuffer, slender, SNDCHAN_AUTO, SNDLEVEL_SCREAMING, SND_CHANGEVOL); + } + else + { + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_move", sBuffer, sizeof(sBuffer), 1); + if (sBuffer[0]) StopSound(slender, SNDCHAN_AUTO, sBuffer); + } + } + + return Plugin_Continue; +} + + +SlenderOnClientStressUpdate(client) +{ + new Float:flStress = g_flPlayerStress[client]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new iBossIndex = 0; iBossIndex < MAX_BOSSES; iBossIndex++) + { + if (NPCGetUniqueID(iBossIndex) == -1) continue; + + new iBossFlags = NPCGetFlags(iBossIndex); + if (iBossFlags & SFF_MARKEDASFAKE || + iBossFlags & SFF_NOTELEPORT) + { + continue; + } + + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iTeleportTarget = EntRefToEntIndex(g_iSlenderTeleportTarget[iBossIndex]); + if (iTeleportTarget && iTeleportTarget != INVALID_ENT_REFERENCE) + { + if (g_bPlayerEliminated[iTeleportTarget] || + DidClientEscape(iTeleportTarget) || + flStress >= g_flSlenderTeleportMaxTargetStress[iBossIndex] || + GetGameTime() >= g_flSlenderTeleportMaxTargetTime[iBossIndex]) + { + // Queue for a new target and mark the old target in the rest period. + new Float:flRestPeriod = GetProfileFloat(sProfile, "teleport_target_rest_period", 15.0); + flRestPeriod = (flRestPeriod * GetRandomFloat(0.92, 1.08)) / (NPCGetAnger(iBossIndex) * g_flRoundDifficultyModifier); + + g_iSlenderTeleportTarget[iBossIndex] = INVALID_ENT_REFERENCE; + g_flSlenderTeleportPlayersRestTime[iBossIndex][iTeleportTarget] = GetGameTime() + flRestPeriod; + g_flSlenderTeleportMaxTargetStress[iBossIndex] = 9999.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = -1.0; + g_flSlenderTeleportTargetTime[iBossIndex] = -1.0; + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: lost target, putting at rest period", iBossIndex); +#endif + } + } + else if (!g_bRoundGrace) + { + new iPreferredTeleportTarget = INVALID_ENT_REFERENCE; + + new Float:flTargetStressMin = GetProfileFloat(sProfile, "teleport_target_stress_min", 0.2); + new Float:flTargetStressMax = GetProfileFloat(sProfile, "teleport_target_stress_max", 0.9); + + new Float:flTargetStress = flTargetStressMax - ((flTargetStressMax - flTargetStressMin) / (g_flRoundDifficultyModifier * NPCGetAnger(iBossIndex))); + + new Float:flPreferredTeleportTargetStress = flTargetStress; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || + !IsPlayerAlive(i) || + g_bPlayerEliminated[i] || + IsClientInGhostMode(i) || + DidClientEscape(i)) + { + continue; + } + + if (g_flPlayerStress[i] < flPreferredTeleportTargetStress) + { + if (g_flSlenderTeleportPlayersRestTime[iBossIndex][i] <= GetGameTime()) + { + iPreferredTeleportTarget = i; + flPreferredTeleportTargetStress = g_flPlayerStress[i]; + } + } + } + + if (iPreferredTeleportTarget && iPreferredTeleportTarget != INVALID_ENT_REFERENCE) + { + // Set our preferred target to the new guy. + new Float:flTargetDuration = GetProfileFloat(sProfile, "teleport_target_persistency_period", 13.0); + new Float:flDeviation = GetRandomFloat(0.92, 1.08); + flTargetDuration = Pow(flDeviation * flTargetDuration, ((g_flRoundDifficultyModifier * (NPCGetAnger(iBossIndex) - 1.0)) / 2.0)) + ((flDeviation * flTargetDuration) - 1.0); + + g_iSlenderTeleportTarget[iBossIndex] = EntIndexToEntRef(iPreferredTeleportTarget); + g_flSlenderTeleportPlayersRestTime[iBossIndex][iPreferredTeleportTarget] = -1.0; + g_flSlenderTeleportMaxTargetTime[iBossIndex] = GetGameTime() + flTargetDuration; + g_flSlenderTeleportTargetTime[iBossIndex] = GetGameTime(); + g_flSlenderTeleportMaxTargetStress[iBossIndex] = flTargetStress; + + iTeleportTarget = iPreferredTeleportTarget; + +#if defined DEBUG + SendDebugMessageToPlayers(DEBUG_BOSS_TELEPORTATION, 0, "Teleport for boss %d: got new target %N", iBossIndex, iPreferredTeleportTarget); +#endif + } + } + } +} + +static GetPageMusicRanges() +{ + ClearArray(g_hPageMusicRanges); + + decl String:sName[64]; + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (sName[0] && !StrContains(sName, "sf2_page_music_", false)) + { + ReplaceString(sName, sizeof(sName), "sf2_page_music_", "", false); + + new String:sPageRanges[2][32]; + ExplodeString(sName, "-", sPageRanges, 2, 32); + + new iIndex = PushArrayCell(g_hPageMusicRanges, EntIndexToEntRef(ent)); + if (iIndex != -1) + { + new iMin = StringToInt(sPageRanges[0]); + new iMax = StringToInt(sPageRanges[1]); + +#if defined DEBUG + DebugMessage("Page range found: entity %d, iMin = %d, iMax = %d", ent, iMin, iMax); +#endif + SetArrayCell(g_hPageMusicRanges, iIndex, iMin, 1); + SetArrayCell(g_hPageMusicRanges, iIndex, iMax, 2); + } + } + } + + // precache + if (GetArraySize(g_hPageMusicRanges) > 0) + { + decl String:sPath[PLATFORM_MAX_PATH]; + + for (new i = 0; i < GetArraySize(g_hPageMusicRanges); i++) + { + ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + PrecacheSound(sPath); + } + } + } + + LogSF2Message("Loaded page music ranges successfully!"); +} + +SetPageCount(iNum) +{ + if (iNum > g_iPageMax) iNum = g_iPageMax; + + new iOldPageCount = g_iPageCount; + g_iPageCount = iNum; + + if (g_iPageCount != iOldPageCount) + { + if (g_iPageCount > iOldPageCount) + { + if (g_hRoundGraceTimer != INVALID_HANDLE) + { + TriggerTimer(g_hRoundGraceTimer); + } + + g_iRoundTime += g_iRoundTimeGainFromPage; + if (g_iRoundTime > g_iRoundTimeLimit) g_iRoundTime = g_iRoundTimeLimit; + + // Increase anger on selected bosses. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new Float:flPageDiff = NPCGetAngerAddOnPageGrabTimeDiff(i); + if (flPageDiff >= 0.0) + { + new iDiff = g_iPageCount - iOldPageCount; + if ((GetGameTime() - g_flPageFoundLastTime) < flPageDiff) + { + NPCAddAnger(i, NPCGetAngerAddOnPageGrab(i) * float(iDiff)); + } + } + } + + g_flPageFoundLastTime = GetGameTime(); + } + + // Notify logic entities. + decl String:sTargetName[64]; + decl String:sFindTargetName[64]; + Format(sFindTargetName, sizeof(sFindTargetName), "sf2_onpagecount_%d", g_iPageCount); + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "logic_relay")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); + if (sTargetName[0] && StrEqual(sTargetName, sFindTargetName, false)) + { + AcceptEntityInput(ent, "Trigger"); + break; + } + } + + new iClients[MAXPLAYERS + 1] = { -1, ... }; + new iClientsNum = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (!g_bPlayerEliminated[i] || IsClientInGhostMode(i)) + { + if (g_iPageCount) + { + iClients[iClientsNum] = i; + iClientsNum++; + } + } + } + + if (g_iPageCount > 0 && g_bRoundHasEscapeObjective && g_iPageCount == g_iPageMax) + { + // Escape initialized! + SetRoundState(SF2RoundState_Escape); + + if (iClientsNum) + { + new iGameTextEscape = GetTextEntity("sf2_escape_message", false); + if (iGameTextEscape != -1) + { + // Custom escape message. + decl String:sMessage[512]; + GetEntPropString(iGameTextEscape, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextEscape, g_hHudSync, sMessage); + } + else + { + // Default escape message. + for (new i = 0; i < iClientsNum; i++) + { + new client = iClients[i]; + ClientShowMainMessage(client, "%d/%d\n%T", g_iPageCount, g_iPageMax, "SF2 Default Escape Message", i); + } + } + } + } + else + { + if (iClientsNum) + { + new iGameTextPage = GetTextEntity("sf2_page_message", false); + if (iGameTextPage != -1) + { + // Custom page message. + decl String:sMessage[512]; + GetEntPropString(iGameTextPage, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameTextPage, g_hHudSync, sMessage, g_iPageCount, g_iPageMax); + } + else + { + // Default page message. + for (new i = 0; i < iClientsNum; i++) + { + new client = iClients[i]; + ClientShowMainMessage(client, "%d/%d", g_iPageCount, g_iPageMax); + } + } + } + } + + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + } +} + +GetTextEntity(const String:sTargetName[], bool:bCaseSensitive=true) +{ + // Try to see if we can use a custom message instead of the default. + decl String:targetName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "game_text")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (StrEqual(targetName, sTargetName, bCaseSensitive)) + { + return ent; + } + } + } + + return -1; +} + +ShowHudTextUsingTextEntity(const iClients[], iClientsNum, iGameText, Handle:hHudSync, const String:sMessage[], ...) +{ + if (!sMessage[0]) return; + if (!IsValidEntity(iGameText)) return; + + decl String:sTrueMessage[512]; + VFormat(sTrueMessage, sizeof(sTrueMessage), sMessage, 6); + + new Float:flX = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.x"); + new Float:flY = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.y"); + new iEffect = GetEntProp(iGameText, Prop_Data, "m_textParms.effect"); + new Float:flFadeInTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime"); + new Float:flFadeOutTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime"); + new Float:flHoldTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); + new Float:flFxTime = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fxTime"); + + new Color1[4] = { 255, 255, 255, 255 }; + new Color2[4] = { 255, 255, 255, 255 }; + + new iParmsOffset = FindDataMapOffs(iGameText, "m_textParms"); + if (iParmsOffset != -1) + { + // hudtextparms_s m_textParms + + Color1[0] = GetEntData(iGameText, iParmsOffset + 12, 1); + Color1[1] = GetEntData(iGameText, iParmsOffset + 13, 1); + Color1[2] = GetEntData(iGameText, iParmsOffset + 14, 1); + Color1[3] = GetEntData(iGameText, iParmsOffset + 15, 1); + + Color2[0] = GetEntData(iGameText, iParmsOffset + 16, 1); + Color2[1] = GetEntData(iGameText, iParmsOffset + 17, 1); + Color2[2] = GetEntData(iGameText, iParmsOffset + 18, 1); + Color2[3] = GetEntData(iGameText, iParmsOffset + 19, 1); + } + + SetHudTextParamsEx(flX, flY, flHoldTime, Color1, Color2, iEffect, flFxTime, flFadeInTime, flFadeOutTime); + + for (new i = 0; i < iClientsNum; i++) + { + new iClient = iClients[i]; + if (!IsValidClient(iClient) || IsFakeClient(iClient)) continue; + + ShowSyncHudText(iClient, hHudSync, sTrueMessage); + } +} + +// ========================================================== +// EVENT HOOKS +// ========================================================== + +public Event_RoundStart(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundStart"); +#endif + + // Reset some global variables. + g_iRoundCount++; + g_hRoundTimer = INVALID_HANDLE; + + SetRoundState(SF2RoundState_Invalid); + + SetPageCount(0); + g_iPageMax = 0; + g_flPageFoundLastTime = GetGameTime(); + + g_hVoteTimer = INVALID_HANDLE; + + // Remove all bosses from the game. + NPCRemoveAll(); + + // Refresh groups. + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + SetPlayerGroupPlaying(i, false); + CheckPlayerGroup(i); + } + + // Refresh players. + for (new i = 1; i <= MaxClients; i++) + { + ClientSetGhostModeState(i, false); + + g_bPlayerPlaying[i] = false; + g_bPlayerEliminated[i] = true; + g_bPlayerEscaped[i] = false; + } + + // Calculate the new round state. + if (g_bRoundWaitingForPlayers) + { + SetRoundState(SF2RoundState_Waiting); + } + else if (GetConVarBool(g_cvWarmupRound) && g_iRoundWarmupRoundCount < GetConVarInt(g_cvWarmupRoundNum)) + { + g_iRoundWarmupRoundCount++; + + SetRoundState(SF2RoundState_Waiting); + + ServerCommand("mp_restartgame 15"); + PrintCenterTextAll("Round restarting in 15 seconds"); + } + else + { + g_iRoundActiveCount++; + + InitializeNewGame(); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundStart"); +#endif +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_RoundEnd"); +#endif + + SetRoundState(SF2RoundState_Outro); + + DistributeQueuePointsToPlayers(); + + g_iRoundEndCount++; + CheckRoundLimitForBossPackVote(g_iRoundEndCount); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_RoundEnd"); +#endif +} + +static DistributeQueuePointsToPlayers() +{ + // Give away queue points. + new iDefaultAmount = 5; + new iAmount = iDefaultAmount; + new iAmount2 = iAmount; + new Action:iAction = Plugin_Continue; + + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + + if (IsPlayerGroupPlaying(i)) + { + SetPlayerGroupQueuePoints(i, 0); + } + else + { + iAmount = iDefaultAmount; + iAmount2 = iAmount; + iAction = Plugin_Continue; + + Call_StartForward(fOnGroupGiveQueuePoints); + Call_PushCell(i); + Call_PushCellRef(iAmount2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) iAmount = iAmount2; + + SetPlayerGroupQueuePoints(i, GetPlayerGroupQueuePoints(i) + iAmount); + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClient(iClient)) continue; + if (ClientGetPlayerGroup(iClient) == i) + { + CPrintToChat(iClient, "%T", "SF2 Give Group Queue Points", iClient, iAmount); + } + } + } + } + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (g_bPlayerPlaying[i]) + { + ClientSetQueuePoints(i, 0); + } + else + { + if (!IsClientParticipating(i)) + { + CPrintToChat(i, "%T", "SF2 No Queue Points To Spectator", i); + } + else + { + iAmount = iDefaultAmount; + iAmount2 = iAmount; + iAction = Plugin_Continue; + + Call_StartForward(fOnClientGiveQueuePoints); + Call_PushCell(i); + Call_PushCellRef(iAmount2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) iAmount = iAmount2; + + ClientSetQueuePoints(i, g_iPlayerQueuePoints[i] + iAmount); + CPrintToChat(i, "%T", "SF2 Give Queue Points", i, iAmount); + } + } + } +} + +public Action:Event_PlayerTeamPre(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerTeamPre"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + if (GetEventInt(event, "team") > 1 || GetEventInt(event, "oldteam") > 1) SetEventBroadcast(event, true); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerTeamPre"); +#endif + + return Plugin_Continue; +} + +public Event_PlayerTeam(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerTeam"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + new iNewTeam = GetEventInt(event, "team"); + if (iNewTeam <= _:TFTeam_Spectator) + { + if (g_bRoundGrace) + { + if (g_bPlayerPlaying[client] && !g_bPlayerEliminated[client]) + { + ForceInNextPlayersInQueue(1, true); + } + } + + // You're not playing anymore. + if (g_bPlayerPlaying[client]) + { + ClientSetQueuePoints(client, 0); + } + + g_bPlayerPlaying[client] = false; + g_bPlayerEliminated[client] = true; + g_bPlayerEscaped[client] = false; + + ClientSetGhostModeState(client, false); + + if (!bool:GetEntProp(client, Prop_Send, "m_bIsCoaching")) + { + // This is to prevent player spawn spam when someone is coaching. Who coaches in SF2, anyway? + TF2_RespawnPlayer(client); + } + + // Special round. + if (g_bSpecialRound) g_bPlayerPlayedSpecialRound[client] = true; + + // Boss round. + if (g_bNewBossRound) g_bPlayerPlayedNewBossRound[client] = true; + } + else + { + if (!g_bPlayerChoseTeam[client]) + { + g_bPlayerChoseTeam[client] = true; + + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + EmitSoundToClient(client, SF2_PROJECTED_FLASHLIGHT_CONFIRM_SOUND); + CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Projected{olive}."); + } + else + { + CPrintToChat(client, "{olive}Your flashlight mode has been set to {lightgreen}Normal{olive}."); + } + + CreateTimer(5.0, Timer_WelcomeMessage, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + } + + // Check groups. + if (!IsRoundEnding()) + { + for (new i = 0; i < SF2_MAX_PLAYER_GROUPS; i++) + { + if (!IsPlayerGroupActive(i)) continue; + CheckPlayerGroup(i); + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerTeam"); +#endif + +} + +/** + * Sets the player to the correct team if needed. Returns true if a change was necessary, false if no change occurred. + */ +static bool:HandlePlayerTeam(client, bool:bRespawn=true) +{ + if (!IsClientInGame(client) || !IsClientParticipating(client)) return false; + + if (!g_bPlayerEliminated[client]) + { + if (GetClientTeam(client) != _:TFTeam_Red) + { + if (bRespawn) + ChangeClientTeamNoSuicide(client, _:TFTeam_Red); + else + ChangeClientTeam(client, _:TFTeam_Red); + + return true; + } + } + else + { + if (GetClientTeam(client) != _:TFTeam_Blue) + { + if (bRespawn) + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + else + ChangeClientTeam(client, _:TFTeam_Blue); + + return true; + } + } + + return false; +} + +static HandlePlayerIntroState(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client) || !IsClientParticipating(client)) return; + + if (!IsRoundInIntro()) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START HandlePlayerIntroState(%d)", client); +#endif + + // Disable movement on player. + SetEntityFlags(client, GetEntityFlags(client) | FL_FROZEN); + + new Float:flDelay = 0.0; + if (!IsFakeClient(client)) + { + flDelay = GetClientLatency(client, NetFlow_Outgoing); + } + + CreateTimer(flDelay * 4.0, Timer_IntroBlackOut, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END HandlePlayerIntroState(%d)", client); +#endif +} + +HandlePlayerHUD(client) +{ + if (IsRoundInWarmup() || IsClientInGhostMode(client)) + { + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + else + { + if (!g_bPlayerEliminated[client]) + { + if (!DidClientEscape(client)) + { + // Player is in the game; disable normal HUD. + SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); + } + else + { + // Player isn't in the game; enable normal HUD behavior. + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + } + else + { + if (g_bPlayerProxy[client]) + { + // Player is in the game; disable normal HUD. + SetEntProp(client, Prop_Send, "m_iHideHUD", HIDEHUD_CROSSHAIR | HIDEHUD_HEALTH); + } + else + { + // Player isn't in the game; enable normal HUD behavior. + SetEntProp(client, Prop_Send, "m_iHideHUD", 0); + } + } + } +} + +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerSpawn(%d)", client); +#endif + + if (!IsClientParticipating(client)) + { + ClientSetGhostModeState(client, false); + } + + g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; + + g_hOverlayUpdateTimer[client] = INVALID_HANDLE; + g_iGhostNextHelpPhrase[client] = 0; + + if (IsPlayerAlive(client) && IsClientParticipating(client)) + { + if (HandlePlayerTeam(client)) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("client->HandlePlayerTeam()"); +#endif + } + else + { + g_iPlayerPageCount[client] = 0; + + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientUpdateListeningFlags(client); + ClientUpdateMusicSystem(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + ClientResetProxy(client); + ClientResetHints(client); + ClientResetScare(client); + + ClientResetDeathCam(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + + ClientHandleGhostMode(client); + + if (!g_bPlayerEliminated[client]) + { + ClientStartDrainingBlinkMeter(client); + ClientSetScareBoostEndTime(client, -1.0); + + ClientStartCampingTimer(client); + + HandlePlayerIntroState(client); + + // screen overlay timer + g_hPlayerOverlayCheck[client] = CreateTimer(0.0, Timer_PlayerOverlayCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerOverlayCheck[client], true); + + if (DidClientEscape(client)) + { + CreateTimer(0.1, Timer_TeleportPlayerToEscapePoint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + ClientEnableConstantGlow(client, "head"); + ClientActivateUltravision(client); + CreateTimer(0.1, Timer_CreateSpriteOverlay, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + } + else + { + g_hPlayerOverlayCheck[client] = INVALID_HANDLE; + } + + g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + HandlePlayerHUD(client); + } + } + + PvP_OnPlayerSpawn(client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerSpawn(%d)", client); +#endif +} + +public Action:Timer_UpdateOverlayTime(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + new String:Time[64], String:DigitBuffer[2]; + FormatTime(Time, sizeof(Time), "%d"); // Day + Format(DigitBuffer, 2, "%s", Time[0]); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit0), StringToFloat(DigitBuffer)); + Format(DigitBuffer, 2, "%s", Time[1]); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit1), StringToFloat(DigitBuffer)); + FormatTime(Time, sizeof(Time), "%m"); // Month + Format(DigitBuffer, 2, "%s", Time[0]); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit3), StringToFloat(DigitBuffer)); + Format(DigitBuffer, 2, "%s", Time[1]); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit4), StringToFloat(DigitBuffer)); + FormatTime(Time, sizeof(Time), "%H"); // Hours + Format(DigitBuffer, 2, "%s", Time[0]); + Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit0), StringToFloat(DigitBuffer)); + Format(DigitBuffer, 2, "%s", Time[1]); + Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit1), StringToFloat(DigitBuffer)); + FormatTime(Time, sizeof(Time), "%M"); // Minutes + Format(DigitBuffer, 2, "%s", Time[0]); + Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit3), StringToFloat(DigitBuffer)); + Format(DigitBuffer, 2, "%s", Time[1]); + Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit4), StringToFloat(DigitBuffer)); + if(g_hRoundTimer != INVALID_HANDLE) { + //g_iRoundTime + //g_iRoundTime = g_iRoundTimeLimit;g_iPageCount == g_iPageMax + new time = g_iRoundTime; + decl maxTime; + if(g_iPageCount == g_iPageMax) { + maxTime = g_iRoundEscapeTimeLimit; + } else { + maxTime = g_iRoundTimeLimit; + } + if(float(time) / float(maxTime) >= 0.75) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 0.0); + else if(float(time) / float(maxTime) >= 0.5) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 1.0); + else if(float(time) / float(maxTime) >= 0.25) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 2.0); + else if(float(time) / float(maxTime) >= 0.17) Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); + else { + Overlay_Color_Red(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[0]); + Overlay_Color_Green(OverlayRef_ByLayer(client, _:HudBattery), 0.0); + Overlay_Color_Blue(OverlayRef_ByLayer(client, _:HudBattery), 0.0); + if(g_bHudBatteryToggle[client]) Overlay_Hide(OverlayRef_ByLayer(client, _:HudBattery));// Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); + else { + Overlay_Show(OverlayRef_ByLayer(client, _:HudBattery)); + Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 3.0); + } + //PrintToServer("%d", g_bHudBatteryToggle[client]); + g_bHudBatteryToggle[client] = !g_bHudBatteryToggle[client]; + } + //Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), float(g_iRoundTime)); + /* + Overlay_Frame(OverlayRef_ByLayer(client, _:HudBattery), 0.0); + Overlay_Color_Red(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[0]); + Overlay_Color_Green(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[1]); + Overlay_Color_Blue(OverlayRef_ByLayer(client, _:HudBattery), g_fHudDigitIntensity[1]); + */ + } +} + +public Action:Timer_CreateSpriteOverlay(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + /* + PrintToServer("Add_Sprite()"); + Add_Sprite(client, OverlayGeneric, 14.0); + //Overlay_Render(client, _:OverlayGeneric, g_fOverlayMatOffset[OverlayGeneric], g_sOverlayMat[OverlayGeneric], -5.0); + new spr; + new String:cl[32]; + PrintToServer("Overlay_Layer_Exists(client, _:OverlayGeneric): %d", Overlay_Layer_Exists(client, _:OverlayGeneric, spr)); + Overlay_Show(spr); + GetEntityClassname(spr, cl, sizeof(cl)); + PrintToServer("spr: %d (%s)", spr, cl); + */ + g_fOverlayMatRotation[OverlayGeneric][2] = GetRandomFloat(0.0, 180.0); + Add_Sprite(client, OverlayGeneric, 35.0); + Add_Sprite(client, Crosshair, 3.0); + + Add_Sprite(client, DateDigit0, g_fHudDigitScale); + Add_Sprite(client, DateDigit1, g_fHudDigitScale); + Add_Sprite(client, DateDigit2, g_fHudDigitScale); + Add_Sprite(client, DateDigit3, g_fHudDigitScale); + Add_Sprite(client, DateDigit4, g_fHudDigitScale); + Add_Sprite(client, DateDigit5, g_fHudDigitScale); + Add_Sprite(client, DateDigit6, g_fHudDigitScale); + Add_Sprite(client, DateDigit7, g_fHudDigitScale); + Add_Sprite(client, ClockDigit0, g_fHudDigitScale); + Add_Sprite(client, ClockDigit1, g_fHudDigitScale); + Add_Sprite(client, ClockDigit2, g_fHudDigitScale); + Add_Sprite(client, ClockDigit3, g_fHudDigitScale); + Add_Sprite(client, ClockDigit4, g_fHudDigitScale); + + Add_Sprite(client, HudRec, g_fHudDigitScale); + Add_Sprite(client, HudBattery, g_fHudDigitScale); + g_bHudBatteryToggle[client] = true; + //PrintToServer("%d", Overlay_Exists(OverlayRef_ByLayer(client, _:DateDigit0))); + for(new i = _:DateDigit0; i <= _:HudBattery; i++) { + Overlay_Framerate(OverlayRef_ByLayer(client, i), 0.0); + Overlay_Color_Red(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[0]); + Overlay_Color_Green(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[1]); + Overlay_Color_Blue(OverlayRef_ByLayer(client, i), g_fHudDigitIntensity[1]); + } + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit2), 10.0); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit5), 10.0); + Overlay_Frame(OverlayRef_ByLayer(client, _:ClockDigit2), 11.0); + /* + new Float:yearDigits[2], String:buffer[16]; + Format(buffer, 16, "0000%s", g_sCameraYear); + strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); + Format(buffer, 1, "%s", g_sCameraYear[strlen(g_sCameraYear)-1]); + strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); + yearDigits[0] = StringToFloat(buffer[0]); + Format(buffer, 1, "%s", g_sCameraYear[strlen(g_sCameraYear)-1]); + strcopy(g_sCameraYear, sizeof(g_sCameraYear), buffer); + yearDigits[1] = StringToFloat(buffer[0]); + */ + //PrintToServer("%s %d %s %s %s", g_sCameraYear, strlen(g_sCameraYear), g_sCameraYear[strlen(g_sCameraYear)-1], g_sCameraYear[strlen(g_sCameraYear)-2], g_sCameraYear[strlen(g_sCameraYear)-3]); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit6), 8.0); + Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit7), 6.0); + g_hOverlayUpdateTimer[client] = CreateTimer(1.2, Timer_UpdateOverlayTime, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); +} + +public Action:Timer_IntroBlackOut(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!IsRoundInIntro()) return; + + if (!IsPlayerAlive(client) || g_bPlayerEliminated[client]) return; + + // Black out the player's screen. + new iFadeFlags = FFADE_OUT | FFADE_STAYOUT | FFADE_PURGE; + UTIL_ScreenFade(client, 0, FixedUnsigned16(90.0, 1 << 12), iFadeFlags, g_iRoundIntroFadeColor[0], g_iRoundIntroFadeColor[1], g_iRoundIntroFadeColor[2], g_iRoundIntroFadeColor[3]); +} + +public Event_PostInventoryApplication(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PostInventoryApplication"); +#endif + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + g_hPlayerPostWeaponsTimer[client] = CreateTimer(0.1, Timer_ClientPostWeapons, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PostInventoryApplication"); +#endif +} + +public Action:Event_DontBroadcastToClients(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + if (IsRoundInWarmup()) return Plugin_Continue; + + SetEventBroadcast(event, true); + return Plugin_Continue; +} + +public Action:Event_PlayerDeathPre(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return Plugin_Continue; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT START: Event_PlayerDeathPre"); +#endif + + if (!IsRoundInWarmup()) + { + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client > 0) + { + if (!IsRoundEnding()) + { + if (g_bRoundGrace || g_bPlayerEliminated[client] || IsClientInGhostMode(client)) + { + SetEventBroadcast(event, true); + } + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("EVENT END: Event_PlayerDeathPre"); +#endif + + return Plugin_Continue; +} + +public Event_PlayerHurt(Handle:event, const String:name[], bool:dB) +{ + if (!g_bEnabled) return; + + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerHurt"); +#endif + + ClientDisableFakeLagCompensation(client); + + new attacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (attacker > 0) + { + if (g_bPlayerProxy[attacker]) + { + g_iPlayerProxyControl[attacker] = 100; + } + } + + // Play any sounds, if any. + if (g_bPlayerProxy[client]) + { + new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iProxyMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + if (GetRandomStringFromProfile(sProfile, "sound_proxy_hurt", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_hurt_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_hurt_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_hurt_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_hurt_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_hurt_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerHurt"); +#endif +} + +public Event_PlayerDeath(Handle:event, const String:name[], bool:dB) +{ + new client = GetClientOfUserId(GetEventInt(event, "userid")); + if (client <= 0) return; + + DestroySpriteOverlay(client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerDeath(%d)", client); +#endif + + new bool:bFake = bool:(GetEventInt(event, "death_flags") & TF_DEATHFLAG_DEADRINGER); + new inflictor = GetEventInt(event, "inflictor_entindex"); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("inflictor = %d", inflictor); +#endif + + if (!bFake) + { + ClientDisableFakeLagCompensation(client); + + ClientResetStatic(client); + ClientResetSlenderStats(client); + ClientResetCampingStats(client); + ClientResetOverlay(client); + ClientResetJumpScare(client); + ClientResetInteractiveGlow(client); + ClientDisableConstantGlow(client); + ClientChaseMusicReset(client); + ClientChaseMusicSeeReset(client); + ClientAlertMusicReset(client); + Client20DollarsMusicReset(client); + ClientMusicReset(client); + + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientResetSprint(client); + ClientResetBreathing(client); + ClientResetBlink(client); + ClientResetDeathCam(client); + + ClientUpdateMusicSystem(client); + + PvP_SetPlayerPvPState(client, false, false, false); + + if (IsRoundInWarmup()) + { + CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + if (!g_bPlayerEliminated[client]) + { + if (IsRoundInIntro() || g_bRoundGrace || DidClientEscape(client)) + { + CreateTimer(0.3, Timer_RespawnPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_bPlayerEliminated[client] = true; + g_bPlayerEscaped[client] = false; + g_hPlayerSwitchBlueTimer[client] = CreateTimer(2.5, Timer_PlayerSwitchToBlue, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + ClientCommand(client, "r_screenoverlay %s", STATIC_OVERLAY); + EmitSoundToClient(client, STATIC_SOUND, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + else + { + } + + { + // If this player was killed by a boss, play a sound. + new npcIndex = NPCGetFromEntIndex(inflictor); + if (npcIndex != -1) + { + decl String:npcProfile[SF2_MAX_PROFILE_NAME_LENGTH], String:buffer[PLATFORM_MAX_PATH]; + NPCGetProfile(npcIndex, npcProfile, sizeof(npcProfile)); + + if (GetRandomStringFromProfile(npcProfile, "sound_attack_killed_all", buffer, sizeof(buffer)) && strlen(buffer) > 0) + { + if (!g_bPlayerEliminated[client]) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_HELICOPTER); + } + } + + SlenderPerformVoice(npcIndex, "sound_attack_killed"); + } + } + + CreateTimer(0.2, Timer_CheckRoundWinConditions, _, TIMER_FLAG_NO_MAPCHANGE); + + // Notify to other bosses that this player has died. + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (EntRefToEntIndex(g_iSlenderTarget[i]) == client) + { + g_iSlenderInterruptConditions[i] |= COND_CHASETARGETINVALIDATED; + GetClientAbsOrigin(client, g_flSlenderChaseDeathPosition[i]); + } + } + } + + if (g_bPlayerProxy[client]) + { + // We're a proxy, so play some sounds. + + new iProxyMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + if (iProxyMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iProxyMaster, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + if (GetRandomStringFromProfile(sProfile, "sound_proxy_death", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_death_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_death_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_death_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_death_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_death_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } + + ClientResetProxy(client, false); + ClientUpdateListeningFlags(client); + + // Half-Zatoichi nerf code. + new iKatanaHealthGain = GetConVarInt(g_cvHalfZatoichiHealthGain); + if (iKatanaHealthGain >= 0) + { + new iAttacker = GetClientOfUserId(GetEventInt(event, "attacker")); + if (iAttacker > 0) + { + if (!IsClientInPvP(iAttacker) && (!g_bPlayerEliminated[iAttacker] || g_bPlayerProxy[iAttacker])) + { + decl String:sWeapon[64]; + GetEventString(event, "weapon", sWeapon, sizeof(sWeapon)); + + if (StrEqual(sWeapon, "demokatana")) + { + new iAttackerPreHealth = GetEntProp(iAttacker, Prop_Send, "m_iHealth"); + new Handle:hPack = CreateDataPack(); + WritePackCell(hPack, GetClientUserId(iAttacker)); + WritePackCell(hPack, iAttackerPreHealth + iKatanaHealthGain); + + CreateTimer(0.0, Timer_SetPlayerHealth, hPack, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + } + + g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; + } + + PvP_OnPlayerDeath(client, bFake); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT END: Event_PlayerDeath(%d)", client); +#endif +} + +public Action:Timer_SetPlayerHealth(Handle:timer, any:data) +{ + new Handle:hPack = Handle:data; + ResetPack(hPack); + new iAttacker = GetClientOfUserId(ReadPackCell(hPack)); + new iHealth = ReadPackCell(hPack); + CloseHandle(hPack); + + if (iAttacker <= 0) return; + + SetEntProp(iAttacker, Prop_Data, "m_iHealth", iHealth); + SetEntProp(iAttacker, Prop_Send, "m_iHealth", iHealth); +} + +public Action:Timer_PlayerSwitchToBlue(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSwitchBlueTimer[client]) return; + + ChangeClientTeam(client, _:TFTeam_Blue); +} + +public Action:Timer_RoundStart(Handle:timer) +{ + if (g_iPageMax > 0) + { + new Handle:hArrayClients = CreateArray(); + new iClients[MAXPLAYERS + 1]; + new iClientsNum = 0; + + new iGameText = GetTextEntity("sf2_intro_message", false); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || g_bPlayerEliminated[i]) continue; + + if (iGameText == -1) + { + if (g_iPageMax > 1) + { + ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Plural", i, g_iPageMax); + } + else + { + ClientShowMainMessage(i, "%T", "SF2 Default Intro Message Singular", i, g_iPageMax); + } + } + + PushArrayCell(hArrayClients, GetClientUserId(i)); + iClients[iClientsNum] = i; + iClientsNum++; + } + + // Show difficulty menu. + if (iClientsNum) + { + // Automatically set it to Normal. + SetConVarInt(g_cvDifficulty, Difficulty_Normal); + + g_hVoteTimer = CreateTimer(1.0, Timer_VoteDifficulty, hArrayClients, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hVoteTimer, true); + + if (iGameText != -1) + { + decl String:sMessage[512]; + GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); + } + } + else + { + CloseHandle(hArrayClients); + } + } +} + +public Action:Timer_CheckRoundWinConditions(Handle:timer) +{ + CheckRoundWinConditions(); +} + +public Action:Timer_RoundGrace(Handle:timer) +{ + if (timer != g_hRoundGraceTimer) return; + + g_bRoundGrace = false; + g_hRoundGraceTimer = INVALID_HANDLE; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientParticipating(i)) g_bPlayerEliminated[i] = true; + } + + // Initialize the main round timer. + if (g_iRoundTimeLimit > 0) + { + // Set round time. + g_iRoundTime = g_iRoundTimeLimit; + g_hRoundTimer = CreateTimer(1.0, Timer_RoundTime, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + else + { + // Infinite round time. + g_hRoundTimer = INVALID_HANDLE; + } + + CPrintToChatAll("{olive}%t", "SF2 Grace Period End"); +} + +public Action:Timer_RoundTime(Handle:timer) +{ + if (timer != g_hRoundTimer) return Plugin_Stop; + + if (g_iRoundTime <= 0) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i)) continue; + + decl Float:flBuffer[3]; + GetClientAbsOrigin(i, flBuffer); + SDKHooks_TakeDamage(i, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + } + + return Plugin_Stop; + } + + g_iRoundTime--; + + new hours, minutes, seconds; + FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); + + SetHudTextParams(-1.0, 0.1, + 1.0, + SF2_HUD_TEXT_COLOR_R, SF2_HUD_TEXT_COLOR_G, SF2_HUD_TEXT_COLOR_B, SF2_HUD_TEXT_COLOR_A, + _, + _, + 1.5, 1.5); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; + ShowSyncHudText(i, g_hRoundTimerSync, "%d/%d\n%d:%02d", g_iPageCount, g_iPageMax, minutes, seconds); + } + + return Plugin_Continue; +} + +public Action:Timer_RoundTimeEscape(Handle:timer) +{ + if (timer != g_hRoundTimer) return Plugin_Stop; + + if (g_iRoundTime <= 0) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || g_bPlayerEliminated[i] || IsClientInGhostMode(i) || DidClientEscape(i)) continue; + + decl Float:flBuffer[3]; + GetClientAbsOrigin(i, flBuffer); + ClientStartDeathCam(i, 0, flBuffer); + } + + return Plugin_Stop; + } + + new hours, minutes, seconds; + FloatToTimeHMS(float(g_iRoundTime), hours, minutes, seconds); + + SetHudTextParams(-1.0, 0.1, + 1.0, + SF2_HUD_TEXT_COLOR_R, + SF2_HUD_TEXT_COLOR_G, + SF2_HUD_TEXT_COLOR_B, + SF2_HUD_TEXT_COLOR_A, + _, + _, + 1.5, 1.5); + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i) || (g_bPlayerEliminated[i] && !IsClientInGhostMode(i))) continue; + ShowSyncHudText(i, g_hRoundTimerSync, "%T\n%d:%02d", "SF2 Default Escape Message", i, minutes, seconds); + } + + g_iRoundTime--; + + return Plugin_Continue; +} + +public Action:Timer_VoteDifficulty(Handle:timer, any:data) +{ + new Handle:hArrayClients = Handle:data; + + if (timer != g_hVoteTimer || IsRoundEnding()) + { + CloseHandle(hArrayClients); + return Plugin_Stop; + } + + if (IsVoteInProgress()) return Plugin_Continue; // There's another vote in progess. Wait. + + new iClients[MAXPLAYERS + 1] = { -1, ... }; + new iClientsNum; + for (new i = 0, iSize = GetArraySize(hArrayClients); i < iSize; i++) + { + new iClient = GetClientOfUserId(GetArrayCell(hArrayClients, i)); + if (iClient <= 0) continue; + + iClients[iClientsNum] = iClient; + iClientsNum++; + } + + CloseHandle(hArrayClients); + + VoteMenu(g_hMenuVoteDifficulty, iClients, iClientsNum, 15); + + return Plugin_Stop; +} + +static InitializeMapEntities() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeMapEntities()"); +#endif + + g_bRoundInfiniteFlashlight = false; + g_bRoundInfiniteBlink = false; + g_bRoundInfiniteSprint = false; + g_bRoundHasEscapeObjective = false; + + g_iRoundTimeLimit = GetConVarInt(g_cvTimeLimit); + g_iRoundEscapeTimeLimit = GetConVarInt(g_cvTimeLimitEscape); + g_iRoundTimeGainFromPage = GetConVarInt(g_cvTimeGainFromPageGrab); + + // Reset page reference. + g_bPageRef = false; + strcopy(g_strPageRefModel, sizeof(g_strPageRefModel), ""); + g_flPageRefModelScale = 1.0; + + new Handle:hArray = CreateArray(2); + new Handle:hPageTrie = CreateTrie(); + + decl String:targetName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (!StrContains(targetName, "sf2_maxpages_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_maxpages_", "", false); + g_iPageMax = StringToInt(targetName); + } + else if (!StrContains(targetName, "sf2_page_spawnpoint", false)) + { + if (!StrContains(targetName, "sf2_page_spawnpoint_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_page_spawnpoint_", "", false); + if (targetName[0]) + { + new Handle:hButtStallion = INVALID_HANDLE; + if (!GetTrieValue(hPageTrie, targetName, hButtStallion)) + { + hButtStallion = CreateArray(); + SetTrieValue(hPageTrie, targetName, hButtStallion); + } + + new iIndex = FindValueInArray(hArray, hButtStallion); + if (iIndex == -1) + { + iIndex = PushArrayCell(hArray, hButtStallion); + } + + PushArrayCell(hButtStallion, ent); + SetArrayCell(hArray, iIndex, true, 1); + } + else + { + new iIndex = PushArrayCell(hArray, ent); + SetArrayCell(hArray, iIndex, false, 1); + } + } + else + { + new iIndex = PushArrayCell(hArray, ent); + SetArrayCell(hArray, iIndex, false, 1); + } + } + else if (!StrContains(targetName, "sf2_logic_escape", false)) + { + g_bRoundHasEscapeObjective = true; + } + else if (!StrContains(targetName, "sf2_infiniteflashlight", false)) + { + g_bRoundInfiniteFlashlight = true; + } + else if (!StrContains(targetName, "sf2_infiniteblink", false)) + { + g_bRoundInfiniteBlink = true; + } + else if (!StrContains(targetName, "sf2_infinitesprint", false)) + { + g_bRoundInfiniteSprint = true; + } + else if (!StrContains(targetName, "sf2_time_limit_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_time_limit_", "", false); + g_iRoundTimeLimit = StringToInt(targetName); + + LogSF2Message("Found sf2_time_limit entity, set time limit to %d", g_iRoundTimeLimit); + } + else if (!StrContains(targetName, "sf2_escape_time_limit_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_escape_time_limit_", "", false); + g_iRoundEscapeTimeLimit = StringToInt(targetName); + + LogSF2Message("Found sf2_escape_time_limit entity, set escape time limit to %d", g_iRoundEscapeTimeLimit); + } + else if (!StrContains(targetName, "sf2_time_gain_from_page_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_time_gain_from_page_", "", false); + g_iRoundTimeGainFromPage = StringToInt(targetName); + + LogSF2Message("Found sf2_time_gain_from_page entity, set time gain to %d", g_iRoundTimeGainFromPage); + } + else if (g_iRoundActiveCount == 1 && (!StrContains(targetName, "sf2_maxplayers_", false))) + { + ReplaceString(targetName, sizeof(targetName), "sf2_maxplayers_", "", false); + SetConVarInt(g_cvMaxPlayers, StringToInt(targetName)); + + LogSF2Message("Found sf2_maxplayers entity, set maxplayers to %d", StringToInt(targetName)); + } + else if (!StrContains(targetName, "sf2_boss_override_", false)) + { + ReplaceString(targetName, sizeof(targetName), "sf2_boss_override_", "", false); + SetConVarString(g_cvBossProfileOverride, targetName); + + LogSF2Message("Found sf2_boss_override entity, set boss profile override to %s", targetName); + } + } + } + + // Get a reference entity, if any. + + ent = -1; + while ((ent = FindEntityByClassname(ent, "prop_dynamic")) != -1) + { + if (g_bPageRef) break; + + GetEntPropString(ent, Prop_Data, "m_iName", targetName, sizeof(targetName)); + if (targetName[0]) + { + if (StrEqual(targetName, "sf2_page_model", false)) + { + g_bPageRef = true; + GetEntPropString(ent, Prop_Data, "m_ModelName", g_strPageRefModel, sizeof(g_strPageRefModel)); + g_flPageRefModelScale = 1.0; + } + } + } + + new iPageCount = GetArraySize(hArray); + if (iPageCount) + { + SortADTArray(hArray, Sort_Random, Sort_Integer); + + decl Float:vecPos[3], Float:vecAng[3], Float:vecDir[3]; + decl page; + ent = -1; + + for (new i = 0; i < iPageCount && (i + 1) <= g_iPageMax; i++) + { + if (bool:GetArrayCell(hArray, i, 1)) + { + new Handle:hButtStallion = Handle:GetArrayCell(hArray, i); + ent = GetArrayCell(hButtStallion, GetRandomInt(0, GetArraySize(hButtStallion) - 1)); + } + else + { + ent = GetArrayCell(hArray, i); + } + + GetEntPropVector(ent, Prop_Data, "m_vecAbsOrigin", vecPos); + GetEntPropVector(ent, Prop_Data, "m_angAbsRotation", vecAng); + GetAngleVectors(vecAng, vecDir, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(vecDir, vecDir); + ScaleVector(vecDir, 1.0); + + page = CreateEntityByName("prop_dynamic_override"); + if (page != -1) + { + TeleportEntity(page, vecPos, vecAng, NULL_VECTOR); + DispatchKeyValue(page, "targetname", "sf2_page"); + + if (g_bPageRef) + { + SetEntityModel(page, g_strPageRefModel); + } + else + { + SetEntityModel(page, PAGE_MODEL); + } + + DispatchKeyValue(page, "solid", "2"); + DispatchSpawn(page); + ActivateEntity(page); + SetVariantInt(i); + AcceptEntityInput(page, "Skin"); + AcceptEntityInput(page, "EnableCollision"); + + if (g_bPageRef) + { + SetEntPropFloat(page, Prop_Send, "m_flModelScale", g_flPageRefModelScale); + } + else + { + SetEntPropFloat(page, Prop_Send, "m_flModelScale", PAGE_MODELSCALE); + } + + SDKHook(page, SDKHook_OnTakeDamage, Hook_PageOnTakeDamage); + SDKHook(page, SDKHook_SetTransmit, Hook_SlenderObjectSetTransmit); + } + } + + // Safely remove all handles. + for (new i = 0, iSize = GetArraySize(hArray); i < iSize; i++) + { + if (bool:GetArrayCell(hArray, i, 1)) + { + CloseHandle(Handle:GetArrayCell(hArray, i)); + } + } + + Call_StartForward(fOnPagesSpawned); + Call_Finish(); + } + + CloseHandle(hPageTrie); + CloseHandle(hArray); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeMapEntities()"); +#endif +} + +static HandleSpecialRoundState() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleSpecialRoundState()"); +#endif + + new bool:bOld = g_bSpecialRound; + new bool:bContinuousOld = g_bSpecialRoundContinuous; + g_bSpecialRound = false; + g_bSpecialRoundNew = false; + g_bSpecialRoundContinuous = false; + + new bool:bForceNew = false; + + if (bOld) + { + if (bContinuousOld) + { + // Check if there are players who haven't played the special round yet. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedSpecialRound[i] = true; + continue; + } + + if (!g_bPlayerPlayedSpecialRound[i]) + { + // Someone didn't get to play this yet. Continue the special round. + g_bSpecialRound = true; + g_bSpecialRoundContinuous = true; + break; + } + } + } + } + + new iRoundInterval = GetConVarInt(g_cvSpecialRoundInterval); + + if (iRoundInterval > 0 && g_iSpecialRoundCount >= iRoundInterval) + { + g_bSpecialRound = true; + bForceNew = true; + } + + // Do special round force override and reset it. + if (GetConVarInt(g_cvSpecialRoundForce) >= 0) + { + g_bSpecialRound = GetConVarBool(g_cvSpecialRoundForce); + SetConVarInt(g_cvSpecialRoundForce, -1); + } + + if (g_bSpecialRound) + { + if (bForceNew || !bOld || !bContinuousOld) + { + g_bSpecialRoundNew = true; + } + + if (g_bSpecialRoundNew) + { + if (GetConVarInt(g_cvSpecialRoundBehavior) == 1) + { + g_bSpecialRoundContinuous = true; + } + else + { + // New special round, but it's not continuous. + g_bSpecialRoundContinuous = false; + } + } + } + else + { + g_bSpecialRoundContinuous = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleSpecialRoundState() -> g_bSpecialRound = %d (count = %d, new = %d, continuous = %d)", g_bSpecialRound, g_iSpecialRoundCount, g_bSpecialRoundNew, g_bSpecialRoundContinuous); +#endif +} + +bool:IsNewBossRoundRunning() +{ + return g_bNewBossRound; +} + +/** + * Returns an array which contains all the profile names valid to be chosen for a new boss round. + */ +static Handle:GetNewBossRoundProfileList() +{ + new Handle:hBossList = CloneArray(GetSelectableBossProfileList()); + + if (GetArraySize(hBossList) > 0) + { + decl String:sMainBoss[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossMain, sMainBoss, sizeof(sMainBoss)); + + new index = FindStringInArray(hBossList, sMainBoss); + if (index != -1) + { + // Main boss exists; remove him from the list. + RemoveFromArray(hBossList, index); + } + else + { + // Main boss doesn't exist; remove the first boss from the list. + RemoveFromArray(hBossList, 0); + } + } + + return hBossList; +} + +static HandleNewBossRoundState() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START HandleNewBossRoundState()"); +#endif + + new bool:bOld = g_bNewBossRound; + new bool:bContinuousOld = g_bNewBossRoundContinuous; + g_bNewBossRound = false; + g_bNewBossRoundNew = false; + g_bNewBossRoundContinuous = false; + + new bool:bForceNew = false; + + if (bOld) + { + if (bContinuousOld) + { + // Check if there are players who haven't played the boss round yet. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedNewBossRound[i] = true; + continue; + } + + if (!g_bPlayerPlayedNewBossRound[i]) + { + // Someone didn't get to play this yet. Continue the boss round. + g_bNewBossRound = true; + g_bNewBossRoundContinuous = true; + break; + } + } + } + } + + // Don't force a new special round while a continuous round is going on. + if (!g_bNewBossRoundContinuous) + { + new iRoundInterval = GetConVarInt(g_cvNewBossRoundInterval); + + if (/*iRoundInterval > 0 &&*/ iRoundInterval <= 0 || g_iNewBossRoundCount >= iRoundInterval) + { + g_bNewBossRound = true; + bForceNew = true; + } + } + + // Do boss round force override and reset it. + if (GetConVarInt(g_cvNewBossRoundForce) >= 0) + { + g_bNewBossRound = GetConVarBool(g_cvNewBossRoundForce); + SetConVarInt(g_cvNewBossRoundForce, -1); + } + + // Check if we have enough bosses. + if (g_bNewBossRound) + { + new Handle:hBossList = GetNewBossRoundProfileList(); + + if (GetArraySize(hBossList) < 1) + { + g_bNewBossRound = false; // Not enough bosses. + } + + CloseHandle(hBossList); + } + + if (g_bNewBossRound) + { + if (bForceNew || !bOld || !bContinuousOld) + { + g_bNewBossRoundNew = true; + } + + if (g_bNewBossRoundNew) + { + if (GetConVarInt(g_cvNewBossRoundBehavior) == 1) + { + g_bNewBossRoundContinuous = true; + } + else + { + // New "new boss round", but it's not continuous. + g_bNewBossRoundContinuous = false; + } + } + } + else + { + g_bNewBossRoundContinuous = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END HandleNewBossRoundState() -> g_bNewBossRound = %d (count = %d, new = %d, continuous = %d)", g_bNewBossRound, g_iNewBossRoundCount, g_bNewBossRoundNew, g_bNewBossRoundContinuous); +#endif +} + +/** + * Returns the amount of players that are in game and currently not eliminated. + */ +stock GetActivePlayerCount() +{ + new count = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + count++; + } + } + + return count; +} + +static SelectStartingBossesForRound() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START SelectStartingBossesForRound()"); +#endif + + new Handle:hSelectableBossList = GetSelectableBossProfileList(); + + // Select which boss profile to use. + decl String:sProfileOverride[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossProfileOverride, sProfileOverride, sizeof(sProfileOverride)); + + if (strlen(sProfileOverride) > 0 && IsProfileValid(sProfileOverride)) + { + // Pick the overridden boss. + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfileOverride); + SetConVarString(g_cvBossProfileOverride, ""); + } + else if (g_bNewBossRound) + { + if (g_bNewBossRoundNew) + { + new Handle:hBossList = GetNewBossRoundProfileList(); + + GetArrayString(hBossList, GetRandomInt(0, GetArraySize(hBossList) - 1), g_strNewBossRoundProfile, sizeof(g_strNewBossRoundProfile)); + + CloseHandle(hBossList); + } + + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), g_strNewBossRoundProfile); + } + else + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetConVarString(g_cvBossMain, sProfile, sizeof(sProfile)); + + if (strlen(sProfile) > 0 && IsProfileValid(sProfile)) + { + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), sProfile); + } + else + { + if (GetArraySize(hSelectableBossList) > 0) + { + // Pick the first boss in our array if the main boss doesn't exist. + GetArrayString(hSelectableBossList, 0, g_strRoundBossProfile, sizeof(g_strRoundBossProfile)); + } + else + { + // No bosses to pick. What? + strcopy(g_strRoundBossProfile, sizeof(g_strRoundBossProfile), ""); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END SelectStartingBossesForRound() -> boss: %s", g_strRoundBossProfile); +#endif +} + +static GetRoundIntroParameters() +{ + g_iRoundIntroFadeColor[0] = 0; + g_iRoundIntroFadeColor[1] = 0; + g_iRoundIntroFadeColor[2] = 0; + g_iRoundIntroFadeColor[3] = 255; + + g_flRoundIntroFadeHoldTime = GetConVarFloat(g_cvIntroDefaultHoldTime); + g_flRoundIntroFadeDuration = GetConVarFloat(g_cvIntroDefaultFadeTime); + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "env_fade")) != -1) + { + decl String:sName[32]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_intro_fade", false)) + { + new iColorOffset = FindSendPropOffs("CBaseEntity", "m_clrRender"); + if (iColorOffset != -1) + { + g_iRoundIntroFadeColor[0] = GetEntData(ent, iColorOffset, 1); + g_iRoundIntroFadeColor[1] = GetEntData(ent, iColorOffset + 1, 1); + g_iRoundIntroFadeColor[2] = GetEntData(ent, iColorOffset + 2, 1); + g_iRoundIntroFadeColor[3] = GetEntData(ent, iColorOffset + 3, 1); + } + + g_flRoundIntroFadeHoldTime = GetEntPropFloat(ent, Prop_Data, "m_HoldTime"); + g_flRoundIntroFadeDuration = GetEntPropFloat(ent, Prop_Data, "m_Duration"); + + break; + } + } + + // Get the intro music. + strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), SF2_INTRO_DEFAULT_MUSIC); + + ent = -1; + while ((ent = FindEntityByClassname(ent, "ambient_generic")) != -1) + { + decl String:sName[64]; + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + + if (StrEqual(sName, "sf2_intro_music", false)) + { + decl String:sSongPath[PLATFORM_MAX_PATH]; + GetEntPropString(ent, Prop_Data, "m_iszSound", sSongPath, sizeof(sSongPath)); + + if (strlen(sSongPath) == 0) + { + LogError("Found sf2_intro_music entity, but it has no sound path specified! Default intro music will be used instead."); + } + else + { + strcopy(g_strRoundIntroMusic, sizeof(g_strRoundIntroMusic), sSongPath); + } + + break; + } + } +} + +static GetRoundEscapeParameters() +{ + g_iRoundEscapePointEntity = INVALID_ENT_REFERENCE; + + decl String:sName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (!StrContains(sName, "sf2_escape_spawnpoint", false)) + { + g_iRoundEscapePointEntity = EntIndexToEntRef(ent); + break; + } + } +} + +InitializeNewGame() +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("START InitializeNewGame()"); +#endif + + GetRoundIntroParameters(); + GetRoundEscapeParameters(); + + // Choose round state. + if (GetConVarBool(g_cvIntroEnabled)) + { + // Set the round state to the intro stage. + SetRoundState(SF2RoundState_Intro); + } + else + { + SetRoundState(SF2RoundState_Active); + } + + if (g_iRoundActiveCount == 1) + { + SetConVarString(g_cvBossProfileOverride, ""); + } + + HandleSpecialRoundState(); + + // Was a new special round initialized? + if (g_bSpecialRound) + { + if (g_bSpecialRoundNew) + { + // Reset round count. + g_iSpecialRoundCount = 1; + + if (g_bSpecialRoundContinuous) + { + // It's the start of a continuous special round. + + // Initialize all players' values. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedSpecialRound[i] = true; + continue; + } + + g_bPlayerPlayedSpecialRound[i] = false; + } + } + + SpecialRoundCycleStart(); + } + else + { + SpecialRoundStart(); + + if (g_bSpecialRoundContinuous) + { + // Display the current special round going on to late players. + CreateTimer(3.0, Timer_DisplaySpecialRound, _, TIMER_FLAG_NO_MAPCHANGE); + } + } + } + else + { + g_iSpecialRoundCount++; + + SpecialRoundReset(); + } + + // Determine boss round state. + HandleNewBossRoundState(); + + if (g_bNewBossRound) + { + if (g_bNewBossRoundNew) + { + // Reset round count; + g_iNewBossRoundCount = 1; + + if (g_bNewBossRoundContinuous) + { + // It's the start of a continuous "new boss round". + + // Initialize all players' values. + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsClientParticipating(i)) + { + g_bPlayerPlayedNewBossRound[i] = true; + continue; + } + + g_bPlayerPlayedNewBossRound[i] = false; + } + } + } + } + else + { + g_iNewBossRoundCount++; + } + + InitializeMapEntities(); + + // Initialize pages and entities. + GetPageMusicRanges(); + + SelectStartingBossesForRound(); + + ForceInNextPlayersInQueue(GetMaxPlayersForRound()); + + // Respawn all players, if needed. + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientParticipating(i)) + { + if (!HandlePlayerTeam(i)) + { + if (!g_bPlayerEliminated[i]) + { + // Players currently in the "game" still have to be respawned. + TF2_RespawnPlayer(i); + } + } + } + } + + if (GetRoundState() == SF2RoundState_Intro) + { + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (!g_bPlayerEliminated[i]) + { + if (!IsFakeClient(i)) + { + // Currently in intro state, play intro music. + g_hPlayerIntroMusicTimer[i] = CreateTimer(0.5, Timer_PlayIntroMusicToPlayer, GetClientUserId(i), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; + } + } + else + { + g_hPlayerIntroMusicTimer[i] = INVALID_HANDLE; + } + } + } + else + { + // Spawn the boss! + SelectProfile(0, g_strRoundBossProfile); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("END InitializeNewGame()"); +#endif +} + +public Action:Timer_PlayIntroMusicToPlayer(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerIntroMusicTimer[client]) return; + + g_hPlayerIntroMusicTimer[client] = INVALID_HANDLE; + + EmitSoundToClient(client, g_strRoundIntroMusic, _, MUSIC_CHAN, SNDLEVEL_NONE); +} + +public Action:Timer_IntroTextSequence(Handle:timer) +{ + if (!g_bEnabled) return; + if (g_hRoundIntroTextTimer != timer) return; + + new Float:flDuration = 0.0; + + if (g_iRoundIntroText != 0) + { + new bool:bFoundGameText = false; + + new iClients[MAXPLAYERS + 1]; + new iClientsNum; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || g_bPlayerEliminated[i]) continue; + + iClients[iClientsNum] = i; + iClientsNum++; + } + + if (!g_bRoundIntroTextDefault) + { + decl String:sTargetname[64]; + Format(sTargetname, sizeof(sTargetname), "sf2_intro_text_%d", g_iRoundIntroText); + + new iGameText = FindEntityByTargetname(sTargetname, "game_text"); + if (iGameText && iGameText != INVALID_ENT_REFERENCE) + { + bFoundGameText = true; + flDuration = GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeinTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.fadeoutTime") + GetEntPropFloat(iGameText, Prop_Data, "m_textParms.holdTime"); + + decl String:sMessage[512]; + GetEntPropString(iGameText, Prop_Data, "m_iszMessage", sMessage, sizeof(sMessage)); + ShowHudTextUsingTextEntity(iClients, iClientsNum, iGameText, g_hHudSync, sMessage); + } + } + else + { + if (g_iRoundIntroText == 2) + { + bFoundGameText = false; + + decl String:sMessage[64]; + GetCurrentMap(sMessage, sizeof(sMessage)); + + for (new i = 0; i < iClientsNum; i++) + { + ClientShowMainMessage(iClients[i], sMessage, 1); + } + } + } + + if (g_iRoundIntroText == 1 && !bFoundGameText) + { + // Use default intro sequence. Eugh. + g_bRoundIntroTextDefault = true; + flDuration = GetConVarFloat(g_cvIntroDefaultHoldTime) / 2.0; + + for (new i = 0; i < iClientsNum; i++) + { + EmitSoundToClient(iClients[i], SF2_INTRO_DEFAULT_MUSIC, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + else + { + if (!bFoundGameText) return; // done with sequence; don't check anymore. + } + } + + g_iRoundIntroText++; + g_hRoundIntroTextTimer = CreateTimer(flDuration, Timer_IntroTextSequence, _, TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ActivateRoundFromIntro(Handle:timer) +{ + if (!g_bEnabled) return; + if (g_hRoundIntroTimer != timer) return; + + // Obviously we don't want to spawn the boss when g_strRoundBossProfile isn't set yet. + SetRoundState(SF2RoundState_Active); + + // Spawn the boss! + SelectProfile(0, g_strRoundBossProfile); +} + +CheckRoundWinConditions() +{ + if (IsRoundInWarmup() || IsRoundEnding()) return; + + new iTotalCount = 0; + new iAliveCount = 0; + new iEscapedCount = 0; + + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + iTotalCount++; + if (!g_bPlayerEliminated[i] && !IsClientInDeathCam(i)) + { + iAliveCount++; + if (DidClientEscape(i)) iEscapedCount++; + } + } + + if (iAliveCount == 0) + { + ForceTeamWin(_:TFTeam_Blue); + } + else + { + if (g_bRoundHasEscapeObjective) + { + if (iEscapedCount == iAliveCount) + { + ForceTeamWin(_:TFTeam_Red); + } + } + else + { + if (g_iPageMax > 0 && g_iPageCount == g_iPageMax) + { + ForceTeamWin(_:TFTeam_Red); + } + } + } +} + +// ========================================================== +// API +// ========================================================== + +public Native_IsRunning(Handle:plugin, numParams) +{ + return g_bEnabled; +} + +public Native_GetCurrentDifficulty(Handle:plugin, numParams) +{ + return GetConVarInt(g_cvDifficulty); +} + +public Native_GetDifficultyModifier(Handle:plugin, numParams) +{ + new iDifficulty = GetNativeCell(1); + if (iDifficulty < Difficulty_Easy || iDifficulty >= Difficulty_Max) + { + LogError("Difficulty parameter can only be from %d to %d!", Difficulty_Easy, Difficulty_Max - 1); + return _:1.0; + } + + switch (iDifficulty) + { + case Difficulty_Easy: return _:DIFFICULTY_EASY; + case Difficulty_Hard: return _:DIFFICULTY_HARD; + case Difficulty_Insane: return _:DIFFICULTY_INSANE; + } + + return _:DIFFICULTY_NORMAL; +} + +public Native_IsClientEliminated(Handle:plugin, numParams) +{ + return g_bPlayerEliminated[GetNativeCell(1)]; +} + +public Native_IsClientInGhostMode(Handle:plugin, numParams) +{ + return IsClientInGhostMode(GetNativeCell(1)); +} + +public Native_IsClientProxy(Handle:plugin, numParams) +{ + return g_bPlayerProxy[GetNativeCell(1)]; +} + +public Native_GetClientBlinkCount(Handle:plugin, numParams) +{ + return ClientGetBlinkCount(GetNativeCell(1)); +} + +public Native_GetClientProxyMaster(Handle:plugin, numParams) +{ + return NPCGetFromUniqueID(g_iPlayerProxyMaster[GetNativeCell(1)]); +} + +public Native_GetClientProxyControlAmount(Handle:plugin, numParams) +{ + return g_iPlayerProxyControl[GetNativeCell(1)]; +} + +public Native_GetClientProxyControlRate(Handle:plugin, numParams) +{ + return _:g_flPlayerProxyControlRate[GetNativeCell(1)]; +} + +public Native_SetClientProxyMaster(Handle:plugin, numParams) +{ + g_iPlayerProxyMaster[GetNativeCell(1)] = NPCGetUniqueID(GetNativeCell(2)); +} + +public Native_SetClientProxyControlAmount(Handle:plugin, numParams) +{ + g_iPlayerProxyControl[GetNativeCell(1)] = GetNativeCell(2); +} + +public Native_SetClientProxyControlRate(Handle:plugin, numParams) +{ + g_flPlayerProxyControlRate[GetNativeCell(1)] = Float:GetNativeCell(2); +} + +public Native_IsClientLookingAtBoss(Handle:plugin, numParams) +{ + return g_bPlayerSeesSlender[GetNativeCell(1)][GetNativeCell(2)]; +} + +public Native_CollectAsPage(Handle:plugin, numParams) +{ + CollectPage(GetNativeCell(1), GetNativeCell(2)); +} + +public Native_GetMaxBosses(Handle:plugin, numParams) +{ + return MAX_BOSSES; +} + +public Native_EntIndexToBossIndex(Handle:plugin, numParams) +{ + return NPCGetFromEntIndex(GetNativeCell(1)); +} + +public Native_BossIndexToEntIndex(Handle:plugin, numParams) +{ + return NPCGetEntIndex(GetNativeCell(1)); +} + +public Native_BossIDToBossIndex(Handle:plugin, numParams) +{ + return NPCGetFromUniqueID(GetNativeCell(1)); +} + +public Native_BossIndexToBossID(Handle:plugin, numParams) +{ + return NPCGetUniqueID(GetNativeCell(1)); +} + +public Native_GetBossName(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(GetNativeCell(1), sProfile, sizeof(sProfile)); + + SetNativeString(2, sProfile, GetNativeCell(3)); +} + +public Native_GetBossModelEntity(Handle:plugin, numParams) +{ + return EntRefToEntIndex(g_iSlenderModel[GetNativeCell(1)]); +} + +public Native_GetBossTarget(Handle:plugin, numParams) +{ + return EntRefToEntIndex(g_iSlenderTarget[GetNativeCell(1)]); +} + +public Native_GetBossMaster(Handle:plugin, numParams) +{ + return g_iSlenderCopyMaster[GetNativeCell(1)]; +} + +public Native_GetBossState(Handle:plugin, numParams) +{ + return g_iSlenderState[GetNativeCell(1)]; +} + +public Native_IsBossProfileValid(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + return IsProfileValid(sProfile); +} + +public Native_GetBossProfileNum(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + return GetProfileNum(sProfile, sKeyValue, GetNativeCell(3)); +} + +public Native_GetBossProfileFloat(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + return _:GetProfileFloat(sProfile, sKeyValue, Float:GetNativeCell(3)); +} + +public Native_GetBossProfileString(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + new iResultLen = GetNativeCell(4); + decl String:sResult[iResultLen]; + + decl String:sDefaultValue[512]; + GetNativeString(5, sDefaultValue, sizeof(sDefaultValue)); + + new bool:bSuccess = GetProfileString(sProfile, sKeyValue, sResult, iResultLen, sDefaultValue); + + SetNativeString(3, sResult, iResultLen); + return bSuccess; +} + +public Native_GetBossProfileVector(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + decl Float:flResult[3]; + decl Float:flDefaultValue[3]; + GetNativeArray(4, flDefaultValue, 3); + + new bool:bSuccess = GetProfileVector(sProfile, sKeyValue, flResult, flDefaultValue); + + SetNativeArray(3, flResult, 3); + return bSuccess; +} + +public Native_GetRandomStringFromBossProfile(Handle:plugin, numParams) +{ + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + GetNativeString(1, sProfile, SF2_MAX_PROFILE_NAME_LENGTH); + + decl String:sKeyValue[256]; + GetNativeString(2, sKeyValue, sizeof(sKeyValue)); + + new iBufferLen = GetNativeCell(4); + decl String:sBuffer[iBufferLen]; + + new iIndex = GetNativeCell(5); + + new bool:bSuccess = GetRandomStringFromProfile(sProfile, sKeyValue, sBuffer, iBufferLen, iIndex); + SetNativeString(3, sBuffer, iBufferLen); + return bSuccess; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/rytp_horror/client.sp b/addons/sourcemod/scripting/rytp_horror/client.sp index f3626f5..4e231e5 100644 --- a/addons/sourcemod/scripting/rytp_horror/client.sp +++ b/addons/sourcemod/scripting/rytp_horror/client.sp @@ -1,5888 +1,5925 @@ -#if defined _sf2_client_included - #endinput -#endif -#define _sf2_client_included - -#define GHOST_MODEL "models/props_halloween/ghost_no_hat.mdl" -#define SF2_OVERLAY_DEFAULT "overlays/rytp_horror/grain" -#define SF2_OVERLAY_DEFAULT_NO_FILMGRAIN "overlays/rytp_horror/grain" // TODO: Update material? -#define SF2_OVERLAY_GHOST "overlays/rytp_horror/grain" - -#define SF2_FLASHLIGHT_WIDTH 512.0 // How wide the player's Flashlight should be in world units. -#define SF2_FLASHLIGHT_LENGTH 1024.0 // How far the player's Flashlight can reach in world units. -#define SF2_FLASHLIGHT_BRIGHTNESS 0 // Intensity of the players' Flashlight. -#define SF2_FLASHLIGHT_DRAIN_RATE 0.65 // How long (in seconds) each bar on the player's Flashlight meter lasts. -#define SF2_FLASHLIGHT_RECHARGE_RATE 0.68 // How long (in seconds) it takes each bar on the player's Flashlight meter to recharge. -#define SF2_FLASHLIGHT_FLICKERAT 0.25 // The percentage of the Flashlight battery where the Flashlight will start to blink. -#define SF2_FLASHLIGHT_ENABLEAT 0.3 // The percentage of the Flashlight battery where the Flashlight will be able to be used again (if the player shortens out the Flashlight from excessive use). -#define SF2_FLASHLIGHT_COOLDOWN 0.4 // How much time players have to wait before being able to switch their flashlight on again after turning it off. - -#define SF2_ULTRAVISION_WIDTH 800.0 -#define SF2_ULTRAVISION_LENGTH 800.0 -#define SF2_ULTRAVISION_BRIGHTNESS -4 // Intensity of Ultravision. -#define SF2_ULTRAVISION_CONE 180.0 - -#define SF2_PLAYER_BREATH_COOLDOWN_MIN 0.8 -#define SF2_PLAYER_BREATH_COOLDOWN_MAX 2.0 - -new String:g_strPlayerBreathSounds[][] = -{ - "rytp_horror/player_breath_1.wav" -}; - -static String:g_strPlayerLagCompensationWeapons[][] = -{ - "tf_weapon_sniperrifle", - "tf_weapon_sniperrifle_decap", - "tf_weapon_sniperrifle_classic" -}; - -// Deathcam data. -static g_iPlayerDeathCamBoss[MAXPLAYERS + 1] = { -1, ... }; -static bool:g_bPlayerDeathCam[MAXPLAYERS + 1] = { false, ... }; -static bool:g_bPlayerDeathCamShowOverlay[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerDeathCamEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerDeathCamEnt2[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Handle:g_hPlayerDeathCamTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Flashlight data. -static bool:g_bPlayerFlashlight[MAXPLAYERS + 1] = { false, ... }; -static bool:g_bPlayerFlashlightBroken[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerFlashlightEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerFlashlightEntAng[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Float:g_flPlayerFlashlightBatteryLife[MAXPLAYERS + 1] = { 1.0, ... }; -static Handle:g_hPlayerFlashlightBatteryTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerFlashlightNextInputTime[MAXPLAYERS + 1] = { -1.0, ... }; - -// Ultravision data. -static bool:g_bPlayerUltravision[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerUltravisionEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; - -// Sprint data. -static bool:g_bPlayerSprint[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerSprintPoints[MAXPLAYERS + 1] = { 100, ... }; -static Handle:g_hPlayerSprintTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Blink data. -static Handle:g_hPlayerBlinkTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static bool:g_bPlayerBlink[MAXPLAYERS + 1] = { false, ... }; -static Float:g_flPlayerBlinkMeter[MAXPLAYERS + 1] = { 0.0, ... }; -static g_iPlayerBlinkCount[MAXPLAYERS + 1] = { 0, ... }; - -// Breathing data. -static bool:g_bPlayerBreath[MAXPLAYERS + 1] = { false, ... }; -static Handle:g_hPlayerBreathTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; - -// Interactive glow data. -static g_iPlayerInteractiveGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static g_iPlayerInteractiveGlowTargetEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; - -// Constant glow data. -static g_iPlayerConstantGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static bool:g_bPlayerConstantGlowEnabled[MAXPLAYERS + 1] = { false, ... }; - -// Jumpscare data. -static g_iPlayerJumpScareBoss[MAXPLAYERS + 1] = { -1, ... }; -static Float:g_flPlayerJumpScareLifeTime[MAXPLAYERS + 1] = { -1.0, ... }; - -static Float:g_flPlayerScareBoostEndTime[MAXPLAYERS + 1] = { -1.0, ... }; - -// Anti-camping data. -static g_iPlayerCampingStrikes[MAXPLAYERS + 1] = { 0, ... }; -static Handle:g_hPlayerCampingTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerCampingLastPosition[MAXPLAYERS + 1][3]; -static bool:g_bPlayerCampingFirstTime[MAXPLAYERS + 1] = { true, ... }; - - -// ========================================================== -// GENERAL CLIENT HOOK FUNCTIONS -// ========================================================== - -#define SF2_PLAYER_VIEWBOB_TIMER 10.0 -#define SF2_PLAYER_VIEWBOB_SCALE_X 0.05 -#define SF2_PLAYER_VIEWBOB_SCALE_Y 0.0 -#define SF2_PLAYER_VIEWBOB_SCALE_Z 0.0 - - -public MRESReturn:Hook_ClientWantsLagCompensationOnEntity(thisPointer, Handle:hReturn, Handle:hParams) -{ - if (!g_bEnabled || IsFakeClient(thisPointer)) return MRES_Ignored; - - DHookSetReturn(hReturn, true); - return MRES_Supercede; -} - -Float:ClientGetScareBoostEndTime(client) -{ - return g_flPlayerScareBoostEndTime[client]; -} - -ClientSetScareBoostEndTime(client, Float:time) -{ - g_flPlayerScareBoostEndTime[client] = time; -} - -public Hook_ClientPreThink(client) -{ - if (!g_bEnabled) return; - - ClientProcessViewAngles(client); - ClientProcessVisibility(client); - ClientProcessStaticShake(client); - ClientProcessFlashlightAngles(client); - ClientProcessInteractiveGlow(client); - - if (IsClientInGhostMode(client)) - { - SetEntPropFloat(client, Prop_Send, "m_flNextAttack", GetGameTime() + 2.0); - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 520.0); - } - else if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) - { - if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) - { - new iRoundState = _:GameRules_GetRoundState(); - - // No double jumping for players in play. - SetEntProp(client, Prop_Send, "m_iAirDash", 99999); - - if (!g_bPlayerProxy[client]) - { - if (iRoundState == 4) - { - new bool:bDanger = false; - - if (!bDanger) - { - decl iState; - decl iBossTarget; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetType(i) == SF2BossType_Chaser) - { - iBossTarget = EntRefToEntIndex(g_iSlenderTarget[i]); - iState = g_iSlenderState[i]; - - if ((iState == STATE_CHASE || iState == STATE_ATTACK || iState == STATE_STUN) && - ((iBossTarget && iBossTarget != INVALID_ENT_REFERENCE && (iBossTarget == client || ClientGetDistanceFromEntity(client, iBossTarget) < 512.0)) || NPCGetDistanceFromEntity(i, client) < 512.0 || PlayerCanSeeSlender(client, i, false))) - { - bDanger = true; - ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); - - // Induce client stress levels. - new Float:flUnComfortZoneDist = 512.0; - new Float:flStressScalar = (flUnComfortZoneDist / NPCGetDistanceFromEntity(i, client)); - ClientAddStress(client, 0.025 * flStressScalar); - - break; - } - } - } - } - - if (g_flPlayerStaticAmount[client] > 0.4) bDanger = true; - if (GetGameTime() < ClientGetScareBoostEndTime(client)) bDanger = true; - - if (!bDanger) - { - decl iState; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetType(i) == SF2BossType_Chaser) - { - if (iState == STATE_ALERT) - { - if (PlayerCanSeeSlender(client, i)) - { - bDanger = true; - ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); - } - } - } - } - } - - if (!bDanger) - { - new Float:flCurTime = GetGameTime(); - new Float:flScareSprintDuration = 3.0; - if (TF2_GetPlayerClass(client) == TFClass_DemoMan) flScareSprintDuration *= 1.667; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if ((flCurTime - g_flPlayerScareLastTime[client][i]) <= flScareSprintDuration) - { - bDanger = true; - break; - } - } - } - - new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); - new Float:flSprintSpeed = ClientGetDefaultSprintSpeed(client); - - // Check for weapon speed changes. - new iWeapon = INVALID_ENT_REFERENCE; - - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; - - new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - switch (iItemDef) - { - case 239: // Gloves of Running Urgently - { - if (GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon") == iWeapon) - { - flSprintSpeed += (flSprintSpeed * 0.1); - } - } - case 775: // Escape Plan - { - new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); - new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); - new Float:flPercentage = flHealth / flMaxHealth; - - if (flPercentage < 0.805 && flPercentage >= 0.605) flSprintSpeed += (flSprintSpeed * 0.05); - else if (flPercentage < 0.605 && flPercentage >= 0.405) flSprintSpeed += (flSprintSpeed * 0.1); - else if (flPercentage < 0.405 && flPercentage >= 0.205) flSprintSpeed += (flSprintSpeed * 0.15); - else if (flPercentage < 0.205) flSprintSpeed += (flSprintSpeed * 0.2); - } - } - } - - // Speed buff? - if (TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly)) - { - flWalkSpeed += (flWalkSpeed * 0.08); - flSprintSpeed += (flSprintSpeed * 0.08); - } - - if (bDanger) - { - flWalkSpeed *= 1.33; - flSprintSpeed *= 1.33; - - if (!g_bPlayerHints[client][PlayerHint_Sprint]) - { - ClientShowHint(client, PlayerHint_Sprint); - } - } - - new Float:flSprintSpeedSubtract = ((flSprintSpeed - flWalkSpeed) * 0.5); - flSprintSpeedSubtract -= flSprintSpeedSubtract * (g_iPlayerSprintPoints[client] != 0 ? (float(g_iPlayerSprintPoints[client]) / 100.0) : 0.0); - flSprintSpeed -= flSprintSpeedSubtract; - - if (IsClientSprinting(client)) - { - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flSprintSpeed); - } - else - { - SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flWalkSpeed); - } - - if (ClientCanBreath(client) && !g_bPlayerBreath[client]) - { - ClientStartBreathing(client); - } - } - } - else - { - new TFClassType:iClass = TF2_GetPlayerClass(client); - new bool:bSpeedup = TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly); - - switch (iClass) - { - case TFClass_Scout: - { - if (iRoundState == 4) - { - if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 405.0); - else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); - } - } - case TFClass_Medic: - { - if (iRoundState == 4) - { - if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 385.0); - else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); - } - } - } - } - } - } - - // Calculate player stress levels. - if (GetGameTime() >= g_flPlayerStressNextUpdateTime[client]) - { - //new Float:flPagePercent = g_iPageMax != 0 ? float(g_iPageCount) / float(g_iPageMax) : 0.0; - //new Float:flPageCountPercent = g_iPageMax != 0? float(g_iPlayerPageCount[client]) / float(g_iPageMax) : 0.0; - - g_flPlayerStressNextUpdateTime[client] = GetGameTime() + 0.33; - ClientAddStress(client, -0.01); - -#if defined DEBUG - SendDebugMessageToPlayer(client, DEBUG_PLAYER_STRESS, 1, "g_flPlayerStress[%d]: %0.1f", client, g_flPlayerStress[client]); -#endif - } - - // Process screen shake, if enabled. - if (g_bPlayerShakeEnabled) - { - new bool:bDoShake = false; - - if (IsPlayerAlive(client)) - { - new iStaticMaster = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); - if (iStaticMaster != -1 && NPCGetFlags(iStaticMaster) & SFF_HASVIEWSHAKE) - { - bDoShake = true; - } - } - - if (bDoShake) - { - new Float:flPercent = g_flPlayerStaticAmount[client]; - - new Float:flAmplitudeMax = GetConVarFloat(g_cvPlayerShakeAmplitudeMax); - new Float:flAmplitude = flAmplitudeMax * flPercent; - - new Float:flFrequencyMax = GetConVarFloat(g_cvPlayerShakeFrequencyMax); - new Float:flFrequency = flFrequencyMax * flPercent; - - UTIL_ScreenShake(client, flAmplitude, 0.5, flFrequency); - } - } -} - -public Action:Hook_ClientSetTransmit(client, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (other != client) - { - if (IsClientInGhostMode(client) && !IsClientInGhostMode(other)) return Plugin_Handled; - - if (!IsRoundEnding()) - { - // SPECIAL ROUND: Singleplayer - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (!g_bPlayerEliminated[client] && !g_bPlayerEliminated[other] && !DidClientEscape(other)) return Plugin_Handled; - } - - // pvp - if (IsClientInPvP(client) && IsClientInPvP(other)) - { - if (TF2_IsPlayerInCondition(client, TFCond_Cloaked) && - !TF2_IsPlayerInCondition(client, TFCond_CloakFlicker) && - !TF2_IsPlayerInCondition(client, TFCond_Jarated) && - !TF2_IsPlayerInCondition(client, TFCond_Milked) && - !TF2_IsPlayerInCondition(client, TFCond_OnFire) && - (GetGameTime() > GetEntPropFloat(client, Prop_Send, "m_flInvisChangeCompleteTime"))) - { - return Plugin_Handled; - } - } - } - } - - return Plugin_Continue; -} - -public Action:TF2_CalcIsAttackCritical(client, weapon, String:sWeaponName[], &bool:result) -{ - if (!g_bEnabled) return Plugin_Continue; - - if ((IsRoundInWarmup() || IsClientInPvP(client)) && !IsRoundEnding()) - { - if (!GetConVarBool(g_cvPlayerFakeLagCompensation)) - { - new bool:bNeedsManualDamage = false; - - // Fake lag compensation isn't enabled; check to see if we need to deal damage manually. - for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) - { - if (StrEqual(sWeaponName, g_strPlayerLagCompensationWeapons[i], false)) - { - bNeedsManualDamage = true; - break; - } - } - - if (bNeedsManualDamage) - { - decl Float:flStartPos[3], Float:flEyeAng[3]; - GetClientEyePosition(client, flStartPos); - GetClientEyeAngles(client, flEyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flEyeAng, MASK_SHOT, RayType_Infinite, TraceRayDontHitEntity, client); - new iHitEntity = TR_GetEntityIndex(hTrace); - new iHitGroup = TR_GetHitGroup(hTrace); - CloseHandle(hTrace); - - if (IsValidClient(iHitEntity)) - { - if (GetClientTeam(iHitEntity) == GetClientTeam(client)) - { - if (IsRoundInWarmup() || IsClientInPvP(iHitEntity)) - { - new Float:flChargedDamage = GetEntPropFloat(weapon, Prop_Send, "m_flChargedDamage"); - if (flChargedDamage < 50.0) flChargedDamage = 50.0; - new iDamageType = DMG_BULLET; - - if (IsClientCritBoosted(client)) - { - result = true; - iDamageType |= DMG_ACID; - } - else if (iHitGroup == 1) - { - if (StrEqual(sWeaponName, "tf_weapon_sniperrifle_classic", false)) - { - if (flChargedDamage >= 150.0) - { - result = true; - iDamageType |= DMG_ACID; - } - } - else - { - if (TF2_IsPlayerInCondition(client, TFCond_Zoomed)) - { - result = true; - iDamageType |= DMG_ACID; - } - } - } - - SDKHooks_TakeDamage(iHitEntity, client, client, flChargedDamage, iDamageType); - return Plugin_Changed; - } - } - } - } - } - } - - return Plugin_Continue; -} - -public Action:Hook_ClientOnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (IsRoundInWarmup()) return Plugin_Continue; - - if (attacker != victim && IsValidClient(attacker)) - { - if (!IsRoundEnding()) - { - if (IsClientInPvP(victim) && IsClientInPvP(attacker)) - { - if (attacker == inflictor) - { - if (IsValidEdict(weapon)) - { - decl String:sWeaponClass[64]; - GetEdictClassname(weapon, sWeaponClass, sizeof(sWeaponClass)); - - // Backstab check! - if ((StrEqual(sWeaponClass, "tf_weapon_knife", false) || (TF2_GetPlayerClass(attacker) == TFClass_Spy && StrEqual(sWeaponClass, "saxxy", false))) && - (damagecustom != TF_CUSTOM_TAUNT_FENCING)) - { - decl Float:flMyPos[3], Float:flHisPos[3], Float:flMyDirection[3]; - GetClientAbsOrigin(victim, flMyPos); - GetClientAbsOrigin(attacker, flHisPos); - GetClientEyeAngles(victim, flMyDirection); - GetAngleVectors(flMyDirection, flMyDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flMyDirection, flMyDirection); - ScaleVector(flMyDirection, 32.0); - AddVectors(flMyDirection, flMyPos, flMyDirection); - - decl Float:p[3], Float:s[3]; - MakeVectorFromPoints(flMyPos, flHisPos, p); - MakeVectorFromPoints(flMyPos, flMyDirection, s); - if (GetVectorDotProduct(p, s) <= 0.0) - { - damage = float(GetEntProp(victim, Prop_Send, "m_iHealth")) * 2.0; - - new Handle:hCvar = FindConVar("tf_weapon_criticals"); - if (hCvar != INVALID_HANDLE && GetConVarBool(hCvar)) damagetype |= DMG_ACID; - return Plugin_Changed; - } - } - } - } - } - /* - else if (g_bPlayerProxy[victim] || g_bPlayerProxy[attacker]) - { - if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) - { - damage = 0.0; - return Plugin_Changed; - } - - if (g_bPlayerProxy[attacker]) - { - new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, victim); - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[attacker]); - if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) - { - if (damagecustom == TF_CUSTOM_TAUNT_GRAND_SLAM || - damagecustom == TF_CUSTOM_TAUNT_FENCING || - damagecustom == TF_CUSTOM_TAUNT_ARROW_STAB || - damagecustom == TF_CUSTOM_TAUNT_GRENADE || - damagecustom == TF_CUSTOM_TAUNT_BARBARIAN_SWING || - damagecustom == TF_CUSTOM_TAUNT_ENGINEER_ARM || - damagecustom == TF_CUSTOM_TAUNT_ARMAGEDDON) - { - if (damage >= float(iMaxHealth)) damage = float(iMaxHealth) * 0.5; - else damage = 0.0; - } - else if (damagecustom == TF_CUSTOM_BACKSTAB) // Modify backstab damage. - { - damage = float(iMaxHealth) * GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy_backstab", 0.25); - if (damagetype & DMG_ACID) damage /= 3.0; - } - - g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitenemy"); - if (g_iPlayerProxyControl[attacker] > 100) - { - g_iPlayerProxyControl[attacker] = 100; - } - - damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy", 1.0); - } - - return Plugin_Changed; - } - else if (g_bPlayerProxy[victim]) - { - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[victim]); - if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) - { - g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitbyenemy"); - if (g_iPlayerProxyControl[attacker] > 100) - { - g_iPlayerProxyControl[attacker] = 100; - } - - damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_self", 1.0); - } - - return Plugin_Changed; - } - } - */ - else - { - damage = 0.0; - return Plugin_Changed; - } - } - else - { - if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) - { - damage = 0.0; - return Plugin_Changed; - } - } - - if (IsClientInGhostMode(victim)) - { - damage = 0.0; - return Plugin_Changed; - } - } - - return Plugin_Continue; -} - -public Action:Hook_TEFireBullets(const String:te_name[], const Players[], numClients, Float:delay) -{ - if (!g_bEnabled) return Plugin_Continue; - - new client = TE_ReadNum("m_iPlayer") + 1; - if (IsValidClient(client)) - { - if (GetConVarBool(g_cvPlayerFakeLagCompensation)) - { - if ((IsRoundInWarmup() || IsClientInPvP(client))) - { - ClientEnableFakeLagCompensation(client); - } - } - } - - return Plugin_Continue; -} - -ClientResetStatic(client) -{ - g_iPlayerStaticMaster[client] = -1; - g_hPlayerStaticTimer[client] = INVALID_HANDLE; - g_flPlayerStaticIncreaseRate[client] = 0.0; - g_flPlayerStaticDecreaseRate[client] = 0.0; - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - g_flPlayerLastStaticTime[client] = 0.0; - g_flPlayerLastStaticVolume[client] = 0.0; - g_bPlayerInStaticShake[client] = false; - g_iPlayerStaticShakeMaster[client] = -1; - g_flPlayerStaticShakeMinVolume[client] = 0.0; - g_flPlayerStaticShakeMaxVolume[client] = 0.0; - g_flPlayerStaticAmount[client] = 0.0; - - if (IsClientInGame(client)) - { - if (g_strPlayerStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticSound[client]); - if (g_strPlayerLastStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - if (g_strPlayerStaticShakeSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); - strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), ""); - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); -} - -ClientResetHints(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetHints(%d)", client); -#endif - - for (new i = 0; i < PlayerHint_MaxNum; i++) - { - g_bPlayerHints[client][i] = false; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetHints(%d)", client); -#endif -} - -ClientShowHint(client, iHint) -{ - g_bPlayerHints[client][iHint] = true; - - switch (iHint) - { - case PlayerHint_Sprint: PrintHintText(client, "%T", "SF2 Hint Sprint", client); - case PlayerHint_Flashlight: PrintHintText(client, "%T", "SF2 Hint Flashlight", client); - case PlayerHint_Blink: PrintHintText(client, "%T", "SF2 Hint Blink", client); - case PlayerHint_MainMenu: PrintHintText(client, "%T", "SF2 Hint Main Menu", client); - } -} - -bool:DidClientEscape(client) -{ - return g_bPlayerEscaped[client]; -} - -ClientEscape(client) -{ - if (DidClientEscape(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("START ClientEscape(%d)", client); -#endif - - g_bPlayerEscaped[client] = true; - - ClientResetBreathing(client); - ClientResetSprint(client); - ClientResetFlashlight(client); - ClientDeactivateUltravision(client); - ClientDisableConstantGlow(client); - - // Speed recalculation. Props to the creators of FF2/VSH for this snippet. - TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); - - HandlePlayerHUD(client); - - decl String:sName[MAX_NAME_LENGTH]; - GetClientName(client, sName, sizeof(sName)); - CPrintToChatAll("%t", "SF2 Player Escaped", sName); - - CheckRoundWinConditions(); - - Call_StartForward(fOnClientEscape); - Call_PushCell(client); - Call_Finish(); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("END ClientEscape(%d)", client); -#endif -} - -public Action:Timer_TeleportPlayerToEscapePoint(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!DidClientEscape(client)) return; - - if (IsPlayerAlive(client)) - { - TeleportClientToEscapePoint(client); - } -} - -stock Float:ClientGetDistanceFromEntity(client, entity) -{ - decl Float:flStartPos[3], Float:flEndPos[3]; - GetClientAbsOrigin(client, flStartPos); - GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", flEndPos); - return GetVectorDistance(flStartPos, flEndPos); -} - -ClientEnableFakeLagCompensation(client) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client) || g_bPlayerLagCompensation[client]) return; - - // Can only enable lag compensation if we're in either of these two teams only. - new iMyTeam = GetClientTeam(client); - if (iMyTeam != _:TFTeam_Red && iMyTeam != _:TFTeam_Blue) return; - - // Can only enable lag compensation if there are other active teammates around. This is to prevent spontaneous round restarting. - new iCount; - for (new i = 1; i <= MaxClients; i++) - { - if (i == client) continue; - - if (IsValidClient(i) && IsPlayerAlive(i)) - { - new iTeam = GetClientTeam(i); - if ((iTeam == _:TFTeam_Red || iTeam == _:TFTeam_Blue) && iTeam == iMyTeam) - { - iCount++; - } - } - } - - if (!iCount) return; - - // Can only enable lag compensation only for specific weapons. - new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); - if (!IsValidEdict(iActiveWeapon)) return; - - decl String:sClassName[64]; - GetEdictClassname(iActiveWeapon, sClassName, sizeof(sClassName)); - - new bool:bCompensate = false; - for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) - { - if (StrEqual(sClassName, g_strPlayerLagCompensationWeapons[i], false)) - { - bCompensate = true; - break; - } - } - - if (!bCompensate) return; - - g_bPlayerLagCompensation[client] = true; - g_iPlayerLagCompensationTeam[client] = iMyTeam; - SetEntProp(client, Prop_Send, "m_iTeamNum", 0); -} - -ClientDisableFakeLagCompensation(client) -{ - if (!g_bPlayerLagCompensation[client]) return; - - SetEntProp(client, Prop_Send, "m_iTeamNum", g_iPlayerLagCompensationTeam[client]); - g_bPlayerLagCompensation[client] = false; - g_iPlayerLagCompensationTeam[client] = -1; -} - -// ========================================================== -// FLASHLIGHT / ULTRAVISION FUNCTIONS -// ========================================================== - -bool:IsClientUsingFlashlight(client) -{ - return g_bPlayerFlashlight[client]; -} - -Float:ClientGetFlashlightBatteryLife(client) -{ - return g_flPlayerFlashlightBatteryLife[client]; -} - -ClientSetFlashlightBatteryLife(client, Float:flPercent) -{ - g_flPlayerFlashlightBatteryLife[client] = flPercent; -} - -/** - * Called in Hook_ClientPreThink, this makes sure the flashlight is oriented correctly on the player. - */ -static ClientProcessFlashlightAngles(client) -{ - if (!IsClientInGame(client)) return; - - if (IsPlayerAlive(client)) - { - decl fl, Float:eyeAng[3], Float:ang2[3]; - - if (IsClientUsingFlashlight(client)) - { - fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - TeleportEntity(fl, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }, NULL_VECTOR); - } - - fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - GetClientEyeAngles(client, eyeAng); - GetClientAbsAngles(client, ang2); - SubtractVectors(eyeAng, ang2, eyeAng); - TeleportEntity(fl, NULL_VECTOR, eyeAng, NULL_VECTOR); - } - } - } -} - -/** - * Handles whether or not the player's flashlight should be "flickering", a sign of a dying flashlight battery. - */ -static ClientHandleFlashlightFlickerState(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) - { - new bool:bFlicker = bool:(ClientGetFlashlightBatteryLife(client) <= SF2_FLASHLIGHT_FLICKERAT); - - new fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - if (bFlicker) - { - SetEntProp(fl, Prop_Data, "m_LightStyle", 10); - } - else - { - SetEntProp(fl, Prop_Data, "m_LightStyle", 0); - } - } - - fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (fl && fl != INVALID_ENT_REFERENCE) - { - if (bFlicker) - { - SetEntityRenderFx(fl, RenderFx:13); - } - else - { - SetEntityRenderFx(fl, RenderFx:0); - } - } - } -} - -bool:IsClientFlashlightBroken(client) -{ - return g_bPlayerFlashlightBroken[client]; -} - -Float:ClientGetFlashlightNextInputTime(client) -{ - return g_flPlayerFlashlightNextInputTime[client]; -} - -/** - * Breaks the player's flashlight. Nothing else. - */ -ClientBreakFlashlight(client) -{ - if (IsClientFlashlightBroken(client)) return; - - g_bPlayerFlashlightBroken[client] = true; - - ClientSetFlashlightBatteryLife(client, 0.0); - ClientTurnOffFlashlight(client); - - ClientAddStress(client, 0.2); - - EmitSoundToAll(FLASHLIGHT_BREAKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - - Call_StartForward(fOnClientBreakFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Resets everything of the player's flashlight. - */ -ClientResetFlashlight(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetFlashlight(%d)", client); -#endif - - ClientTurnOffFlashlight(client); - ClientSetFlashlightBatteryLife(client, 1.0); - g_bPlayerFlashlightBroken[client] = false; - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - g_flPlayerFlashlightNextInputTime[client] = -1.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetFlashlight(%d)", client); -#endif -} - -public Action:Hook_FlashlightSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEnt[other]) != ent) return Plugin_Handled; - - // We've already checked for flashlight ownership in the last statement. So we can do just this. - if (g_iPlayerPreferences[other][PlayerPreference_ProjectedFlashlight]) return Plugin_Handled; - - return Plugin_Continue; -} - -public Action:Hook_Flashlight2SetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[other]) == ent) return Plugin_Handled; - return Plugin_Continue; -} - -public Hook_FlashlightEndSpawnPost(ent) -{ - if (!g_bEnabled) return; - - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightEndSetTransmit); - SDKUnhook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); -} - -public Action:Hook_FlashlightBeamSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - new iSpotlight = -1; - while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) - { - if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) - { - iOwner = iSpotlight; - break; - } - } - - if (iOwner == -1) return Plugin_Continue; - - new iClient = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) - { - iClient = i; - break; - } - } - - if (iClient == -1) return Plugin_Continue; - - if (iClient == other) - { - if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) - { - return Plugin_Handled; - } - } - else - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Hook_FlashlightEndSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - new iSpotlight = -1; - while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) - { - if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) - { - iOwner = iSpotlight; - break; - } - } - - if (iOwner == -1) return Plugin_Continue; - - new iClient = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - - if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) - { - iClient = i; - break; - } - } - - if (iClient == -1) return Plugin_Continue; - - if (iClient == other) - { - if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) - { - return Plugin_Handled; - } - } - else - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - return Plugin_Handled; - } - } - - return Plugin_Continue; -} - -public Action:Timer_DrainFlashlight(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; - - new iOverride = GetConVarInt(g_cvPlayerInfiniteFlashlightOverride); - if ((!g_bRoundInfiniteFlashlight && iOverride != 1) || iOverride == 0) - { - ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) - 0.01); - } - - if (ClientGetFlashlightBatteryLife(client) <= 0.0) - { - // Break the player's flashlight, but also start recharging. - ClientBreakFlashlight(client); - ClientStartRechargingFlashlightBattery(client); - ClientActivateUltravision(client); - return Plugin_Stop; - } - else - { - ClientHandleFlashlightFlickerState(client); - } - - return Plugin_Continue; -} - -public Action:Timer_RechargeFlashlight(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; - - ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) + 0.01); - - if (IsClientFlashlightBroken(client) && ClientGetFlashlightBatteryLife(client) >= SF2_FLASHLIGHT_ENABLEAT) - { - // Repair the flashlight. - g_bPlayerFlashlightBroken[client] = false; - } - - if (ClientGetFlashlightBatteryLife(client) >= 1.0) - { - // I am fully charged! - ClientSetFlashlightBatteryLife(client, 1.0); - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - - return Plugin_Stop; - } - - return Plugin_Continue; -} - -/** - * Turns on the player's flashlight. Nothing else. - */ -ClientTurnOnFlashlight(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) return; - - g_bPlayerFlashlight[client] = true; - - decl Float:flEyePos[3]; - GetClientEyePosition(client, flEyePos); - - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - // If the player is using the projected flashlight, just set effect flags. - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (!(iEffects & (1 << 2))) - { - SetEntProp(client, Prop_Send, "m_fEffects", iEffects | (1 << 2)); - } - } - else - { - // Spawn the light which only the user will see. - new ent = CreateEntityByName("light_dynamic"); - if (ent != -1) - { - TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); - DispatchKeyValue(ent, "targetname", "WUBADUBDUBMOTHERBUCKERS"); - DispatchKeyValue(ent, "rendercolor", "255 255 255"); - SetVariantFloat(SF2_FLASHLIGHT_WIDTH); - AcceptEntityInput(ent, "spotlight_radius"); - SetVariantFloat(SF2_FLASHLIGHT_LENGTH); - AcceptEntityInput(ent, "distance"); - SetVariantInt(SF2_FLASHLIGHT_BRIGHTNESS); - AcceptEntityInput(ent, "brightness"); - - // Convert WU to inches. - new Float:cone = 55.0; - cone *= 0.75; - - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_inner_cone"); - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_cone"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "TurnOn"); - - g_iPlayerFlashlightEnt[client] = EntIndexToEntRef(ent); - - SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightSetTransmit); - } - } - - // Spawn the light that only everyone else will see. - new ent = CreateEntityByName("point_spotlight"); - if (ent != -1) - { - TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); - - decl String:sBuffer[256]; - FloatToString(SF2_FLASHLIGHT_LENGTH, sBuffer, sizeof(sBuffer)); - DispatchKeyValue(ent, "spotlightlength", sBuffer); - FloatToString(SF2_FLASHLIGHT_WIDTH, sBuffer, sizeof(sBuffer)); - DispatchKeyValue(ent, "spotlightwidth", sBuffer); - DispatchKeyValue(ent, "rendercolor", "255 255 255"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "LightOn"); - - g_iPlayerFlashlightEntAng[client] = EntIndexToEntRef(ent); - } - - Call_StartForward(fOnClientActivateFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Turns off the player's flashlight. Nothing else. - */ -ClientTurnOffFlashlight(client) -{ - if (!IsClientUsingFlashlight(client)) return; - - g_bPlayerFlashlight[client] = false; - g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; - - // Remove user-only light. - new ent = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "TurnOff"); - AcceptEntityInput(ent, "Kill"); - } - - // Remove everyone-else-only light. - ent = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "LightOff"); - CreateTimer(0.1, Timer_KillEntity, g_iPlayerFlashlightEntAng[client], TIMER_FLAG_NO_MAPCHANGE); - } - - g_iPlayerFlashlightEnt[client] = INVALID_ENT_REFERENCE; - g_iPlayerFlashlightEntAng[client] = INVALID_ENT_REFERENCE; - - if (IsClientInGame(client)) - { - if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) - { - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (iEffects & (1 << 2)) - { - SetEntProp(client, Prop_Send, "m_fEffects", iEffects &= ~(1 << 2)); - } - } - } - - Call_StartForward(fOnClientDeactivateFlashlight); - Call_PushCell(client); - Call_Finish(); -} - -ClientStartRechargingFlashlightBattery(client) -{ - g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(SF2_FLASHLIGHT_RECHARGE_RATE, Timer_RechargeFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStartDrainingFlashlightBattery(client) -{ - new Float:flDrainRate = SF2_FLASHLIGHT_DRAIN_RATE; - if (TF2_GetPlayerClass(client) == TFClass_Engineer) - { - // Engineers have a 33% longer battery life, basically. - // TODO: Make this value customizable via cvar. - flDrainRate *= 1.33; - } - - g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(flDrainRate, Timer_DrainFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -ClientHandleFlashlight(client) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client)) return; - - if (IsClientUsingFlashlight(client)) - { - ClientTurnOffFlashlight(client); - ClientStartRechargingFlashlightBattery(client); - ClientActivateUltravision(client); - - g_flPlayerFlashlightNextInputTime[client] = GetGameTime() + SF2_FLASHLIGHT_COOLDOWN; - - EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - } - else - { - // Only players in the "game" can use the flashlight. - if (!g_bPlayerEliminated[client]) - { - new bool:bCanUseFlashlight = true; - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_LIGHTSOUT) - { - // Unequip the flashlight please. - bCanUseFlashlight = false; - } - - if (!IsClientFlashlightBroken(client) && bCanUseFlashlight) - { - ClientTurnOnFlashlight(client); - ClientStartDrainingFlashlightBattery(client); - ClientDeactivateUltravision(client); - - g_flPlayerFlashlightNextInputTime[client] = GetGameTime(); - - EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); - } - else - { - EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); - } - } - } -} - -bool:IsClientUsingUltravision(client) -{ - return g_bPlayerUltravision[client]; -} - -ClientActivateUltravision(client) -{ - if (!IsClientInGame(client) || IsClientUsingUltravision(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientActivateUltravision(%d)", client); -#endif - - g_bPlayerUltravision[client] = true; - - new ent = CreateEntityByName("light_dynamic"); - if (ent != -1) - { - decl Float:flEyePos[3]; - GetClientEyePosition(client, flEyePos); - - TeleportEntity(ent, flEyePos, Float:{ 90.0, 0.0, 0.0 }, NULL_VECTOR); - DispatchKeyValue(ent, "rendercolor", "0 200 255"); - - new Float:flRadius = 0.0; - if (g_bPlayerEliminated[client]) - { - flRadius = GetConVarFloat(g_cvUltravisionRadiusBlue); - } - else - { - flRadius = GetConVarFloat(g_cvUltravisionRadiusRed); - } - - SetVariantFloat(flRadius); - AcceptEntityInput(ent, "spotlight_radius"); - SetVariantFloat(flRadius); - AcceptEntityInput(ent, "distance"); - - SetVariantInt(-15); // Start dark, then fade in via the Timer_UltravisionFadeInEffect timer func. - AcceptEntityInput(ent, "brightness"); - - // Convert WU to inches. - new Float:cone = SF2_ULTRAVISION_CONE; - cone *= 0.75; - - SetVariantInt(RoundToFloor(cone)); - AcceptEntityInput(ent, "_inner_cone"); - SetVariantInt(0); - AcceptEntityInput(ent, "_cone"); - DispatchSpawn(ent); - ActivateEntity(ent); - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", client); - AcceptEntityInput(ent, "TurnOn"); - SetEntityRenderFx(ent, RENDERFX_SOLID_SLOW); - SetEntityRenderColor(ent, 100, 200, 255, 255); - - g_iPlayerUltravisionEnt[client] = EntIndexToEntRef(ent); - - SDKHook(ent, SDKHook_SetTransmit, Hook_UltravisionSetTransmit); - - // Fade in effect. - CreateTimer(0.0, Timer_UltravisionFadeInEffect, g_iPlayerUltravisionEnt[client], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientActivateUltravision(%d)", client); -#endif -} - -public Action:Timer_UltravisionFadeInEffect(Handle:timer, any:entref) -{ - new ent = EntRefToEntIndex(entref); - if (!ent || ent == INVALID_ENT_REFERENCE) return Plugin_Stop; - - new iBrightness = GetEntProp(ent, Prop_Send, "m_Exponent"); - if (iBrightness >= GetConVarInt(g_cvUltravisionBrightness)) return Plugin_Stop; - - iBrightness++; - SetVariantInt(iBrightness); - AcceptEntityInput(ent, "brightness"); - - return Plugin_Continue; -} - -ClientDeactivateUltravision(client) -{ - if (!IsClientUsingUltravision(client)) return; - - g_bPlayerUltravision[client] = false; - - new ent = EntRefToEntIndex(g_iPlayerUltravisionEnt[client]); - if (ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "TurnOff"); - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerUltravisionEnt[client] = INVALID_ENT_REFERENCE; -} - -public Action:Hook_UltravisionSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (!GetConVarBool(g_cvUltravisionEnabled) || EntRefToEntIndex(g_iPlayerUltravisionEnt[other]) != ent || !IsPlayerAlive(other)) return Plugin_Handled; - return Plugin_Continue; -} - -static Float:ClientGetDefaultWalkSpeed(client) -{ - new Float:flReturn = 190.0; - new Float:flReturn2 = flReturn; - new Action:iAction = Plugin_Continue; - new TFClassType:iClass = TF2_GetPlayerClass(client); - - switch (iClass) - { - case TFClass_Scout: flReturn = 190.0; - case TFClass_Sniper: flReturn = 190.0; - case TFClass_Soldier: flReturn = 190.0; - case TFClass_DemoMan: flReturn = 190.0; - case TFClass_Heavy: flReturn = 190.0; - case TFClass_Medic: flReturn = 190.0; - case TFClass_Pyro: flReturn = 190.0; - case TFClass_Spy: flReturn = 190.0; - case TFClass_Engineer: flReturn = 190.0; - } - - // Call our forward. - Call_StartForward(fOnClientGetDefaultWalkSpeed); - Call_PushCell(client); - Call_PushCellRef(flReturn2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) flReturn = flReturn2; - - return flReturn; -} - -static Float:ClientGetDefaultSprintSpeed(client) -{ - new Float:flReturn = 300.0; - new Float:flReturn2 = flReturn; - new Action:iAction = Plugin_Continue; - new TFClassType:iClass = TF2_GetPlayerClass(client); - - switch (iClass) - { - case TFClass_Scout: flReturn = 300.0; - case TFClass_Sniper: flReturn = 300.0; - case TFClass_Soldier: flReturn = 275.0; - case TFClass_DemoMan: flReturn = 285.0; - case TFClass_Heavy: flReturn = 270.0; - case TFClass_Medic: flReturn = 300.0; - case TFClass_Pyro: flReturn = 300.0; - case TFClass_Spy: flReturn = 300.0; - case TFClass_Engineer: flReturn = 300.0; - } - - // Call our forward. - Call_StartForward(fOnClientGetDefaultSprintSpeed); - Call_PushCell(client); - Call_PushCellRef(flReturn2); - Call_Finish(iAction); - - if (iAction == Plugin_Changed) flReturn = flReturn2; - - return flReturn; -} - -// Static shaking should only affect the x, y portion of the player's view, not roll. -// This is purely for cosmetic effect. - -ClientProcessStaticShake(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - new bool:bOldStaticShake = g_bPlayerInStaticShake[client]; - new iOldStaticShakeMaster = NPCGetFromUniqueID(g_iPlayerStaticShakeMaster[client]); - new iNewStaticShakeMaster = -1; - new Float:flNewStaticShakeMasterAnger = -1.0; - - new Float:flOldPunchAng[3], Float:flOldPunchAngVel[3]; - GetEntDataVector(client, g_offsPlayerPunchAngle, flOldPunchAng); - GetEntDataVector(client, g_offsPlayerPunchAngleVel, flOldPunchAngVel); - - new Float:flNewPunchAng[3], Float:flNewPunchAngVel[3]; - - for (new i = 0; i < 3; i++) - { - flNewPunchAng[i] = flOldPunchAng[i]; - flNewPunchAngVel[i] = flOldPunchAngVel[i]; - } - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (g_iPlayerStaticMode[client][i] != Static_Increase) continue; - if (!(NPCGetFlags(i) & SFF_HASSTATICSHAKE)) continue; - - if (NPCGetAnger(i) > flNewStaticShakeMasterAnger) - { - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - if (iMaster == -1) iMaster = i; - - iNewStaticShakeMaster = iMaster; - flNewStaticShakeMasterAnger = NPCGetAnger(iMaster); - } - } - - if (iNewStaticShakeMaster != -1) - { - g_iPlayerStaticShakeMaster[client] = NPCGetUniqueID(iNewStaticShakeMaster); - - if (iNewStaticShakeMaster != iOldStaticShakeMaster) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iNewStaticShakeMaster, sProfile, sizeof(sProfile)); - - if (g_strPlayerStaticShakeSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - g_flPlayerStaticShakeMinVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_min", 0.0); - g_flPlayerStaticShakeMaxVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_max", 1.0); - - decl String:sStaticSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_shake_local", sStaticSound, sizeof(sStaticSound)); - if (sStaticSound[0]) - { - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), sStaticSound); - } - else - { - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); - } - } - } - - if (g_bPlayerInStaticShake[client]) - { - if (g_flPlayerStaticAmount[client] <= 0.0) - { - g_bPlayerInStaticShake[client] = false; - } - } - else - { - if (iNewStaticShakeMaster != -1) - { - g_bPlayerInStaticShake[client] = true; - } - } - - if (g_bPlayerInStaticShake[client] && !bOldStaticShake) - { - for (new i = 0; i < 2; i++) - { - flNewPunchAng[i] = 0.0; - flNewPunchAngVel[i] = 0.0; - } - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } - else if (!g_bPlayerInStaticShake[client] && bOldStaticShake) - { - for (new i = 0; i < 2; i++) - { - flNewPunchAng[i] = 0.0; - flNewPunchAngVel[i] = 0.0; - } - - g_iPlayerStaticShakeMaster[client] = -1; - - if (g_strPlayerStaticShakeSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); - } - - strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); - - g_flPlayerStaticShakeMinVolume[client] = 0.0; - g_flPlayerStaticShakeMaxVolume[client] = 0.0; - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } - - if (g_bPlayerInStaticShake[client]) - { - if (g_strPlayerStaticShakeSound[client][0]) - { - new Float:flVolume = g_flPlayerStaticAmount[client]; - if (GetRandomFloat(0.0, 1.0) <= 0.35) - { - flVolume = 0.0; - } - else - { - if (flVolume < g_flPlayerStaticShakeMinVolume[client]) - { - flVolume = g_flPlayerStaticShakeMinVolume[client]; - } - - if (flVolume > g_flPlayerStaticShakeMaxVolume[client]) - { - flVolume = g_flPlayerStaticShakeMaxVolume[client]; - } - } - - EmitSoundToClient(client, g_strPlayerStaticShakeSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL | SND_STOP, flVolume); - } - - // Spazz our view all over the place. - for (new i = 0; i < 2; i++) flNewPunchAng[i] = AngleNormalize(GetRandomFloat(0.0, 360.0)); - NormalizeVector(flNewPunchAng, flNewPunchAng); - - new Float:flAngVelocityScalar = 5.0 * g_flPlayerStaticAmount[client]; - if (flAngVelocityScalar < 1.0) flAngVelocityScalar = 1.0; - ScaleVector(flNewPunchAng, flAngVelocityScalar); - - for (new i = 0; i < 2; i++) flNewPunchAngVel[i] = 0.0; - - SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); - } -} - -ClientProcessVisibility(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; - - new String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - new bool:bWasSeeingSlender[MAX_BOSSES]; - new iOldStaticMode[MAX_BOSSES]; - - decl Float:flSlenderPos[3]; - decl Float:flSlenderEyePos[3]; - decl Float:flSlenderOBBCenterPos[3]; - - decl Float:flMyPos[3]; - GetClientAbsOrigin(client, flMyPos); - - for (new i = 0; i < MAX_BOSSES; i++) - { - bWasSeeingSlender[i] = g_bPlayerSeesSlender[client][i]; - iOldStaticMode[i] = g_iPlayerStaticMode[client][i]; - g_bPlayerSeesSlender[client][i] = false; - g_iPlayerStaticMode[client][i] = Static_None; - - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - new iBoss = NPCGetEntIndex(i); - - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - SlenderGetAbsOrigin(i, flSlenderPos); - NPCGetEyePosition(i, flSlenderEyePos); - - decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; - GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flSlenderMins); - GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flSlenderMaxs); - - for (new i2 = 0; i2 < 3; i2++) flSlenderOBBCenterPos[i2] = flSlenderPos[i2] + ((flSlenderMins[i2] + flSlenderMaxs[i2]) / 2.0); - } - - if (IsClientInGhostMode(client)) - { - } - else if (!IsClientInDeathCam(client)) - { - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - - if (!IsPointVisibleToPlayer(client, flSlenderEyePos, true, SlenderUsesBlink(i))) - { - g_bPlayerSeesSlender[client][i] = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, true, SlenderUsesBlink(i)); - } - else - { - g_bPlayerSeesSlender[client][i] = true; - } - - if ((GetGameTime() - g_flPlayerSeesSlenderLastTime[client][i]) > GetProfileFloat(sProfile, "static_on_look_gracetime", 1.0) || - (iOldStaticMode[i] == Static_Increase && g_flPlayerStaticAmount[client] > 0.1)) - { - if ((NPCGetFlags(i) & SFF_STATICONLOOK) && - g_bPlayerSeesSlender[client][i]) - { - if (iCopyMaster != -1) - { - g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; - } - else - { - g_iPlayerStaticMode[client][i] = Static_Increase; - } - } - else if ((NPCGetFlags(i) & SFF_STATICONRADIUS) && - GetVectorDistance(flMyPos, flSlenderPos) <= g_flSlenderStaticRadius[i]) - { - new bool:bNoObstacles = IsPointVisibleToPlayer(client, flSlenderEyePos, false, false); - if (!bNoObstacles) bNoObstacles = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, false); - - if (bNoObstacles) - { - if (iCopyMaster != -1) - { - g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; - } - else - { - g_iPlayerStaticMode[client][i] = Static_Increase; - } - } - } - } - - // Process death cam sequence conditions - if (SlenderKillsOnNear(i)) - { - if (g_flPlayerStaticAmount[client] >= 1.0 || - GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetInstantKillRadius(i)) - { - new bool:bKillPlayer = true; - if (g_flPlayerStaticAmount[client] < 1.0) - { - bKillPlayer = IsPointVisibleToPlayer(client, flSlenderEyePos, false, SlenderUsesBlink(i)); - } - - if (!bKillPlayer) bKillPlayer = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, SlenderUsesBlink(i)); - - if (bKillPlayer) - { - g_flSlenderLastKill[i] = GetGameTime(); - - if (g_flPlayerStaticAmount[client] >= 1.0) - { - ClientStartDeathCam(client, NPCGetFromUniqueID(g_iPlayerStaticMaster[client]), flSlenderPos); - } - else - { - ClientStartDeathCam(client, i, flSlenderPos); - } - } - } - } - } - } - - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); - if (iMaster == -1) iMaster = i; - - // Boss visiblity. - if (g_bPlayerSeesSlender[client][i] && !bWasSeeingSlender[i]) - { - g_flPlayerSeesSlenderLastTime[client][iMaster] = GetGameTime(); - - if (GetGameTime() >= g_flPlayerScareNextTime[client][iMaster]) - { - if (GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetScareRadius(i)) - { - ClientPerformScare(client, iMaster); - - if (NPCHasAttribute(iMaster, "ignite player on scare")) - { - new Float:flValue = NPCGetAttributeValue(iMaster, "ignite player on scare"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - } - else - { - g_flPlayerScareNextTime[client][iMaster] = GetGameTime() + GetProfileFloat(sProfile, "scare_cooldown"); - } - } - - if (NPCGetType(i) == SF2BossType_Static) - { - if (NPCGetFlags(i) & SFF_FAKE) - { - SlenderMarkAsFake(i); - return; - } - } - - Call_StartForward(fOnClientLooksAtBoss); - Call_PushCell(client); - Call_PushCell(i); - Call_Finish(); - } - else if (!g_bPlayerSeesSlender[client][i] && bWasSeeingSlender[i]) - { - g_flPlayerScareLastTime[client][iMaster] = GetGameTime(); - - Call_StartForward(fOnClientLooksAwayFromBoss); - Call_PushCell(client); - Call_PushCell(i); - Call_Finish(); - } - - if (g_bPlayerSeesSlender[client][i]) - { - if (GetGameTime() >= g_flPlayerSightSoundNextTime[client][iMaster]) - { - ClientPerformSightSound(client, i); - } - } - - if (g_iPlayerStaticMode[client][i] == Static_Increase && - iOldStaticMode[i] != Static_Increase) - { - if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) - { - decl String:sLoopSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); - - if (sLoopSound[0]) - { - EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, GetProfileNum(sProfile, "sound_static_loop_local_level", SNDLEVEL_NORMAL), SND_CHANGEVOL, 1.0); - ClientAddStress(client, 0.03); - } - else - { - LogError("Warning! Boss %s supports static loop local sounds, but was given a blank sound path!", sProfile); - } - } - } - else if (g_iPlayerStaticMode[client][i] != Static_Increase && - iOldStaticMode[i] == Static_Increase) - { - if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) - { - if (iBoss && iBoss != INVALID_ENT_REFERENCE) - { - decl String:sLoopSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); - - if (sLoopSound[0]) - { - EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, _, SND_CHANGEVOL | SND_STOP, 0.0); - } - } - } - } - } - - // Initialize static timers. - new iBossLastStatic = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); - new iBossNewStatic = -1; - if (iBossLastStatic != -1 && g_iPlayerStaticMode[client][iBossLastStatic] == Static_Increase) - { - iBossNewStatic = iBossLastStatic; - } - - for (new i = 0; i < MAX_BOSSES; i++) - { - new iStaticMode = g_iPlayerStaticMode[client][i]; - - // Determine new static rates. - if (iStaticMode != Static_Increase) continue; - - if (iBossLastStatic == -1 || - g_iPlayerStaticMode[client][iBossLastStatic] != Static_Increase || - NPCGetAnger(i) > NPCGetAnger(iBossLastStatic)) - { - iBossNewStatic = i; - } - } - - if (iBossNewStatic != -1) - { - new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossNewStatic]); - if (iCopyMaster != -1) - { - iBossNewStatic = iCopyMaster; - g_iPlayerStaticMaster[client] = NPCGetUniqueID(iCopyMaster); - } - else - { - g_iPlayerStaticMaster[client] = NPCGetUniqueID(iBossNewStatic); - } - } - else - { - g_iPlayerStaticMaster[client] = -1; - } - - if (iBossNewStatic != iBossLastStatic) - { - if (!StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) - { - // Stop last-last static sound entirely. - if (g_strPlayerLastStaticSound[client][0]) - { - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - } - } - - // Move everything down towards the last arrays. - if (g_strPlayerStaticSound[client][0]) - { - strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), g_strPlayerStaticSound[client]); - } - - if (iBossNewStatic == -1) - { - // No one is the static master. - g_hPlayerStaticTimer[client] = CreateTimer(g_flPlayerStaticDecreaseRate[client], - Timer_ClientDecreaseStatic, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerStaticTimer[client], true); - } - else - { - NPCGetProfile(iBossNewStatic, sProfile, sizeof(sProfile)); - - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); - - new String:sStaticSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_static", sStaticSound, sizeof(sStaticSound), 1); - - if (sStaticSound[0]) - { - strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), sStaticSound); - } - - // Cross-fade out the static sounds. - g_flPlayerLastStaticVolume[client] = g_flPlayerStaticAmount[client]; - g_flPlayerLastStaticTime[client] = GetGameTime(); - - g_hPlayerLastStaticTimer[client] = CreateTimer(0.0, - Timer_ClientFadeOutLastStaticSound, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerLastStaticTimer[client], true); - - // Start up our own static timer. - new Float:flStaticIncreaseRate = GetProfileFloat(sProfile, "static_rate") / g_flRoundDifficultyModifier; - new Float:flStaticDecreaseRate = GetProfileFloat(sProfile, "static_rate_decay"); - - g_flPlayerStaticIncreaseRate[client] = flStaticIncreaseRate; - g_flPlayerStaticDecreaseRate[client] = flStaticDecreaseRate; - - g_hPlayerStaticTimer[client] = CreateTimer(flStaticIncreaseRate, - Timer_ClientIncreaseStatic, - GetClientUserId(client), - TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - - TriggerTimer(g_hPlayerStaticTimer[client], true); - } - } -} - -ClientProcessViewAngles(client) -{ - if ((!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) && - !DidClientEscape(client)) - { - // Process view bobbing, if enabled. - // This code is based on the code in this page: https://developer.valvesoftware.com/wiki/Camera_Bob - // Many thanks to whomever created it in the first place. - - if (IsPlayerAlive(client)) - { - if (g_bPlayerViewbobEnabled) - { - new Float:flPunchVel[3]; - - if (!g_bPlayerViewbobSprintEnabled || !IsClientReallySprinting(client)) - { - if (GetEntityFlags(client) & FL_ONGROUND) - { - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - new Float:flSpeed = GetVectorLength(flVelocity); - - new Float:flPunchIdle[3]; - - if (flSpeed > 0.0) - { - if (flSpeed >= 60.0) - { - flPunchIdle[0] = Sine(GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_X / 400.0; - flPunchIdle[1] = Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Y / 400.0; - flPunchIdle[2] = Sine(1.6 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - - // Calculate roll. - decl Float:flForward[3], Float:flVelocityDirection[3]; - GetClientEyeAngles(client, flForward); - GetVectorAngles(flVelocity, flVelocityDirection); - - new Float:flYawDiff = AngleDiff(flForward[1], flVelocityDirection[1]); - if (FloatAbs(flYawDiff) > 90.0) flYawDiff = AngleDiff(flForward[1] + 180.0, flVelocityDirection[1]) * -1.0; - - new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); - new Float:flRollScalar = flSpeed / flWalkSpeed; - if (flRollScalar > 1.0) flRollScalar = 1.0; - - new Float:flRollScale = (flYawDiff / 90.0) * 0.25 * flRollScalar; - flPunchIdle[0] = 0.0; - flPunchIdle[1] = 0.0; - flPunchIdle[2] = flRollScale * -1.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - - /* - if (flSpeed < 60.0) - { - flPunchIdle[0] = FloatAbs(Cosine(GetGameTime() * 1.25) * 0.047); - flPunchIdle[1] = Sine(GetGameTime() * 1.25) * 0.075; - flPunchIdle[2] = 0.0; - - AddVectors(flPunchVel, flPunchIdle, flPunchVel); - } - */ - } - } - - if (g_bPlayerViewbobHurtEnabled) - { - // Shake screen the more the player is hurt. - new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); - new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); - - decl Float:flPunchVelHurt[3]; - flPunchVelHurt[0] = Sine(1.22 * GetGameTime()) * 48.5 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - flPunchVelHurt[1] = Sine(2.12 * GetGameTime()) * 80.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - flPunchVelHurt[2] = Sine(0.5 * GetGameTime()) * 36.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; - - AddVectors(flPunchVel, flPunchVelHurt, flPunchVel); - } - - ClientViewPunch(client, flPunchVel); - } - } - } -} - -public Action:Timer_ClientIncreaseStatic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; - - g_flPlayerStaticAmount[client] += 0.05; - if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; - - if (g_strPlayerStaticSound[client][0]) - { - EmitSoundToClient(client, g_strPlayerStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerStaticAmount[client]); - - if (g_flPlayerStaticAmount[client] >= 0.5) ClientAddStress(client, 0.03); - else - { - ClientAddStress(client, 0.02); - } - } - - return Plugin_Continue; -} - -public Action:Timer_ClientDecreaseStatic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; - - g_flPlayerStaticAmount[client] -= 0.05; - if (g_flPlayerStaticAmount[client] < 0.0) g_flPlayerStaticAmount[client] = 0.0; - - if (g_strPlayerLastStaticSound[client][0]) - { - new Float:flVolume = g_flPlayerStaticAmount[client]; - if (flVolume > 0.0) - { - EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); - } - } - - if (g_flPlayerStaticAmount[client] <= 0.0) - { - // I've done my job; no point to keep on doing it. - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - g_hPlayerStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_ClientFadeOutLastStaticSound(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerLastStaticTimer[client]) return Plugin_Stop; - - if (StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) - { - // Wait, the player's current static sound is the same one we're stopping. Abort! - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - if (g_strPlayerLastStaticSound[client][0]) - { - new Float:flDiff = (GetGameTime() - g_flPlayerLastStaticTime[client]) / 1.0; - if (flDiff > 1.0) flDiff = 1.0; - - new Float:flVolume = g_flPlayerLastStaticVolume[client] - flDiff; - if (flVolume < 0.0) flVolume = 0.0; - - if (flVolume <= 0.0) - { - // I've done my job; no point to keep on doing it. - StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - else - { - EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); - } - } - else - { - // I've done my job; no point to keep on doing it. - g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -// ========================================================== -// INTERACTIVE GLOW FUNCTIONS -// ========================================================== - -static ClientProcessInteractiveGlow(client) -{ - if (!IsClientInGame(client) || !IsPlayerAlive(client) || (g_bPlayerEliminated[client] && !g_bPlayerProxy[client]) || IsClientInGhostMode(client)) return; - - new iOldLookEntity = EntRefToEntIndex(g_iPlayerInteractiveGlowTargetEntity[client]); - - decl Float:flStartPos[3], Float:flMyEyeAng[3]; - GetClientEyePosition(client, flStartPos); - GetClientEyeAngles(client, flMyEyeAng); - - new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flMyEyeAng, MASK_VISIBLE, RayType_Infinite, TraceRayDontHitPlayers, -1); - new iEnt = TR_GetEntityIndex(hTrace); - CloseHandle(hTrace); - - if (IsValidEntity(iEnt)) - { - g_iPlayerInteractiveGlowTargetEntity[client] = EntRefToEntIndex(iEnt); - } - else - { - g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; - } - - if (iEnt != iOldLookEntity) - { - ClientRemoveInteractiveGlow(client); - - if (IsEntityClassname(iEnt, "prop_dynamic", false)) - { - decl String:sTargetName[64]; - GetEntPropString(iEnt, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); - - if (StrContains(sTargetName, "sf2_page", false) == 0 || StrContains(sTargetName, "sf2_interact", false) == 0) - { - ClientCreateInteractiveGlow(client, iEnt); - } - } - } -} - -ClientResetInteractiveGlow(client) -{ - ClientRemoveInteractiveGlow(client); - g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; -} - -/** - * Removes the player's current interactive glow entity. - */ -ClientRemoveInteractiveGlow(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientRemoveInteractiveGlow(%d)", client); -#endif - - new ent = EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerInteractiveGlowEntity[client] = INVALID_ENT_REFERENCE; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientRemoveInteractiveGlow(%d)", client); -#endif -} - -/** - * Creates an interactive glow for an entity to show to a player. - */ -bool:ClientCreateInteractiveGlow(client, iEnt, const String:sAttachment[]="") -{ - ClientRemoveInteractiveGlow(client); - - if (!IsClientInGame(client)) return false; - - if (!iEnt || !IsValidEdict(iEnt)) return false; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientCreateInteractiveGlow(%d)", client); -#endif - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetEntPropString(iEnt, Prop_Data, "m_ModelName", sBuffer, sizeof(sBuffer)); - - if (strlen(sBuffer) == 0) - { - return false; - } - - new ent = CreateEntityByName("tf_taunt_prop"); - if (ent != -1) - { - g_iPlayerInteractiveGlowEntity[client] = EntIndexToEntRef(ent); - - new Float:flModelScale = GetEntPropFloat(iEnt, Prop_Send, "m_flModelScale"); - - SetEntityModel(ent, sBuffer); - DispatchSpawn(ent); - ActivateEntity(ent); - SetEntityRenderMode(ent, RENDER_TRANSCOLOR); - SetEntityRenderColor(ent, 0, 0, 0, 0); - SetEntProp(ent, Prop_Send, "m_bGlowEnabled", 1); - SetEntPropFloat(ent, Prop_Send, "m_flModelScale", flModelScale); - - new iFlags = GetEntProp(ent, Prop_Send, "m_fEffects"); - SetEntProp(ent, Prop_Send, "m_fEffects", iFlags | (1 << 0)); - - SetVariantString("!activator"); - AcceptEntityInput(ent, "SetParent", iEnt); - - if (sAttachment[0]) - { - SetVariantString(sAttachment); - AcceptEntityInput(ent, "SetParentAttachment"); - } - - SDKHook(ent, SDKHook_SetTransmit, Hook_InterativeGlowSetTransmit); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> true", client); -#endif - - return true; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> false", client); -#endif - - return false; -} - -public Action:Hook_InterativeGlowSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[other]) != ent) return Plugin_Handled; - - return Plugin_Continue; -} - -// ========================================================== -// BREATHING FUNCTIONS -// ========================================================== - -ClientResetBreathing(client) -{ - g_bPlayerBreath[client] = false; - g_hPlayerBreathTimer[client] = INVALID_HANDLE; -} - -Float:ClientCalculateBreathingCooldown(client) -{ - new Float:flAverage = 0.0; - new iAverageNum = 0; - - // Sprinting only, for now. - flAverage += (SF2_PLAYER_BREATH_COOLDOWN_MAX * 6.7765 * Pow((float(g_iPlayerSprintPoints[client]) / 100.0), 1.65)); - iAverageNum++; - - flAverage /= float(iAverageNum) - - if (flAverage < SF2_PLAYER_BREATH_COOLDOWN_MIN) flAverage = SF2_PLAYER_BREATH_COOLDOWN_MIN; - - return flAverage; -} - -ClientStartBreathing(client) -{ - g_bPlayerBreath[client] = true; - g_hPlayerBreathTimer[client] = CreateTimer(ClientCalculateBreathingCooldown(client), Timer_ClientBreath, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStopBreathing(client) -{ - g_bPlayerBreath[client] = false; - g_hPlayerBreathTimer[client] = INVALID_HANDLE; -} - -bool:ClientCanBreath(client) -{ - return bool:(ClientCalculateBreathingCooldown(client) < SF2_PLAYER_BREATH_COOLDOWN_MAX); -} - -public Action:Timer_ClientBreath(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerBreathTimer[client]) return; - - if (!g_bPlayerBreath[client]) return; - - if (ClientCanBreath(client)) - { - EmitSoundToAll(g_strPlayerBreathSounds[GetRandomInt(0, sizeof(g_strPlayerBreathSounds) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); - - ClientStartBreathing(client); - return; - } - - ClientStopBreathing(client); -} - -// ========================================================== -// SPRINTING FUNCTIONS -// ========================================================== - -bool:IsClientSprinting(client) -{ - return g_bPlayerSprint[client]; -} - -ClientGetSprintPoints(client) -{ - return g_iPlayerSprintPoints[client]; -} - -ClientResetSprint(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSprint(%d)", client); -#endif - - g_bPlayerSprint[client] = false; - g_iPlayerSprintPoints[client] = 100; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - - if (IsValidClient(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSprint(%d)", client); -#endif -} - -ClientStartSprint(client) -{ - if (IsClientSprinting(client)) return; - - g_bPlayerSprint[client] = true; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - ClientSprintTimer(client); - TriggerTimer(g_hPlayerSprintTimer[client], true); - - SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); -} - -static ClientSprintTimer(client, bool:bRecharge=false) -{ - new Float:flRate = 0.28; - if (bRecharge) flRate = 0.8; - - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - - if (bRecharge) - { - if (!(GetEntityFlags(client) & FL_ONGROUND)) flRate *= 0.75; - else if (GetVectorLength(flVelocity) == 0.0) - { - if (GetEntProp(client, Prop_Send, "m_bDucked")) flRate *= 0.66; - else flRate *= 0.75; - } - } - else - { - if (TF2_GetPlayerClass(client) == TFClass_Scout) flRate *= 1.15; - } - - if (bRecharge) g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientRechargeSprint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - else g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientSprinting, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStopSprint(client) -{ - if (!IsClientSprinting(client)) return; - - g_bPlayerSprint[client] = false; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - ClientSprintTimer(client, true); - - SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); -} - -bool:IsClientReallySprinting(client) -{ - if (!IsClientSprinting(client)) return false; - if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; - - decl Float:flVelocity[3]; - GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); - if (GetVectorLength(flVelocity) < 30.0) return false; - - return true; -} - -public Action:Timer_ClientSprinting(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSprintTimer[client]) return; - - if (!IsClientSprinting(client)) return; - - if (g_iPlayerSprintPoints[client] <= 0) - { - ClientStopSprint(client); - g_iPlayerSprintPoints[client] = 0; - return; - } - - if (IsClientReallySprinting(client)) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); - if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) - { - g_iPlayerSprintPoints[client]--; - } - } - - ClientSprintTimer(client); -} - -public Hook_ClientSprintingPreThink(client) -{ - if (!IsClientReallySprinting(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - return; - } - - new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); - - new iTargetFOV = g_iPlayerDesiredFOV[client] + 10; - - if (iFOV < iTargetFOV) - { - new iDiff = RoundFloat(FloatAbs(float(iFOV - iTargetFOV))); - if (iDiff >= 1) - { - ClientSetFOV(client, iFOV + 1); - } - else - { - ClientSetFOV(client, iTargetFOV); - } - } - else if (iFOV >= iTargetFOV) - { - ClientSetFOV(client, iTargetFOV); - //SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - } -} - -public Hook_ClientRechargeSprintPreThink(client) -{ - if (IsClientReallySprinting(client)) - { - SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); - SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); - return; - } - - new iFOV = GetEntData(client, g_offsPlayerDefaultFOV); - if (iFOV > g_iPlayerDesiredFOV[client]) - { - new iDiff = RoundFloat(FloatAbs(float(iFOV - g_iPlayerDesiredFOV[client]))); - if (iDiff >= 1) - { - ClientSetFOV(client, iFOV - 1); - } - else - { - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } - } - else if (iFOV <= g_iPlayerDesiredFOV[client]) - { - ClientSetFOV(client, g_iPlayerDesiredFOV[client]); - } -} - -public Action:Timer_ClientRechargeSprint(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerSprintTimer[client]) return; - - if (IsClientSprinting(client)) - { - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - return; - } - - if (g_iPlayerSprintPoints[client] >= 100) - { - g_iPlayerSprintPoints[client] = 100; - g_hPlayerSprintTimer[client] = INVALID_HANDLE; - return; - } - - g_iPlayerSprintPoints[client]++; - ClientSprintTimer(client, true); -} - -// ========================================================== -// PROXY / GHOST AND GLOW FUNCTIONS -// ========================================================== - -ClientResetProxy(client, bool:bResetFull=true) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetProxy(%d)", client); -#endif - - new iOldMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - new String:sOldProfileName[SF2_MAX_PROFILE_NAME_LENGTH]; - if (iOldMaster >= 0) - { - NPCGetProfile(iOldMaster, sOldProfileName, sizeof(sOldProfileName)); - } - - new bool:bOldProxy = g_bPlayerProxy[client]; - if (bResetFull) - { - g_bPlayerProxy[client] = false; - g_iPlayerProxyMaster[client] = -1; - } - - g_iPlayerProxyControl[client] = 0; - g_hPlayerProxyControlTimer[client] = INVALID_HANDLE; - g_flPlayerProxyControlRate[client] = 0.0; - g_flPlayerProxyVoiceTimer[client] = INVALID_HANDLE; - - if (IsClientInGame(client)) - { - if (bOldProxy) - { - ClientStartProxyAvailableTimer(client); - - if (bResetFull) - { - SetVariantString(""); - AcceptEntityInput(client, "SetCustomModel"); - } - - if (sOldProfileName[0]) - { - ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_spawn", GetProfileNum(sOldProfileName, "sound_proxy_spawn_channel", SNDCHAN_AUTO)); - ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_hurt", GetProfileNum(sOldProfileName, "sound_proxy_hurt_channel", SNDCHAN_AUTO)); - } - } - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetProxy(%d)", client); -#endif -} - -ClientStartProxyAvailableTimer(client) -{ - g_bPlayerProxyAvailable[client] = false; - g_hPlayerProxyAvailableTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerProxyWaitTime), Timer_ClientProxyAvailable, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); -} - -ClientStartProxyForce(client, iSlenderID, const Float:flPos[3]) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); -#endif - - g_iPlayerProxyAskMaster[client] = iSlenderID; - for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; - - g_iPlayerProxyAvailableCount[client] = 0; - g_bPlayerProxyAvailableInForce[client] = true; - g_hPlayerProxyAvailableTimer[client] = CreateTimer(1.0, Timer_ClientForceProxy, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerProxyAvailableTimer[client], true); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); -#endif -} - -ClientStopProxyForce(client) -{ - g_iPlayerProxyAvailableCount[client] = 0; - g_bPlayerProxyAvailableInForce[client] = false; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; -} - -public Action:Timer_ClientForceProxy(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerProxyAvailableTimer[client]) return Plugin_Stop; - - if (!IsRoundEnding()) - { - new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[client]); - if (iBossIndex != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumProxies = 0; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; - - iNumProxies++; - } - - if (iNumProxies < iMaxProxies) - { - if (g_iPlayerProxyAvailableCount[client] > 0) - { - g_iPlayerProxyAvailableCount[client]--; - - SetHudTextParams(-1.0, 0.25, - 1.0, - 255, 255, 255, 255, - _, - _, - 0.25, 1.25); - - ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Proxy Force Message", client, g_iPlayerProxyAvailableCount[client]); - - return Plugin_Continue; - } - else - { - ClientEnableProxy(client, iBossIndex); - TeleportEntity(client, g_iPlayerProxyAskPosition[client], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - } - } - else - { - PrintToChat(client, "%T", "SF2 Too Many Proxies", client); - } - } - } - - ClientStopProxyForce(client); - return Plugin_Stop; -} - -DisplayProxyAskMenu(client, iAskMaster, const Float:flPos[3]) -{ - decl String:sBuffer[512]; - new Handle:hMenu = CreateMenu(Menu_ProxyAsk); - SetMenuTitle(hMenu, "%T\n \n%T\n \n", "SF2 Proxy Ask Menu Title", client, "SF2 Proxy Ask Menu Description", client); - - Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); - AddMenuItem(hMenu, "1", sBuffer); - Format(sBuffer, sizeof(sBuffer), "%T", "No", client); - AddMenuItem(hMenu, "0", sBuffer); - - g_iPlayerProxyAskMaster[client] = iAskMaster; - for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; - DisplayMenu(hMenu, client, 15); -} - -public Menu_ProxyAsk(Handle:menu, MenuAction:action, param1, param2) -{ - switch (action) - { - case MenuAction_End: CloseHandle(menu); - case MenuAction_Select: - { - if (!IsRoundEnding()) - { - new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[param1]); - if (iBossIndex != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); - new iNumProxies; - - for (new iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; - if (!g_bPlayerProxy[iClient]) continue; - if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; - - iNumProxies++; - } - - if (iNumProxies < iMaxProxies) - { - if (param2 == 0) - { - ClientEnableProxy(param1, iBossIndex); - TeleportEntity(param1, g_iPlayerProxyAskPosition[param1], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - } - else - { - ClientStartProxyAvailableTimer(param1); - } - } - else - { - PrintToChat(param1, "%T", "SF2 Too Many Proxies", param1); - } - } - } - } - } -} - -public Action:Timer_ClientProxyAvailable(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerProxyAvailableTimer[client]) return; - - g_bPlayerProxyAvailable[client] = true; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; -} - -ClientEnableProxy(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) return; - if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) return; - if (g_bPlayerProxy[client]) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - PvP_SetPlayerPvPState(client, false, false, false); - - ClientSetGhostModeState(client, false); - - ClientStopProxyForce(client); - - ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); - if (!IsPlayerAlive(client)) TF2_RespawnPlayer(client); - // Speed recalculation. Props to the creators of FF2/VSH for this snippet. - TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); - - g_bPlayerProxy[client] = true; - g_iPlayerProxyMaster[client] = NPCGetUniqueID(iBossIndex); - g_iPlayerProxyControl[client] = 100; - g_flPlayerProxyControlRate[client] = GetProfileFloat(sProfile, "proxies_controldrainrate"); - g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - g_bPlayerProxyAvailable[client] = false; - g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; - - decl String:sAllowedClasses[512]; - GetProfileString(sProfile, "proxies_classes", sAllowedClasses, sizeof(sAllowedClasses)); - - decl String:sClassName[64]; - TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); - if (sAllowedClasses[0] && sClassName[0] && StrContains(sAllowedClasses, sClassName, false) == -1) - { - // Pick the first class that's allowed. - new String:sAllowedClassesList[32][32]; - new iClassCount = ExplodeString(sAllowedClasses, " ", sAllowedClassesList, 32, 32); - if (iClassCount) - { - TF2_SetPlayerClass(client, TF2_GetClass(sAllowedClassesList[0]), _, false); - - new iMaxHealth = GetEntProp(client, Prop_Send, "m_iHealth"); - TF2_RegeneratePlayer(client); - SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); - SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); - } - } - - UTIL_ScreenFade(client, 200, 1, FFADE_IN, 255, 255, 255, 100); - PrecacheSound("weapons/teleporter_send.wav"); - EmitSoundToClient(client, "weapons/teleporter_send.wav", _, SNDCHAN_STATIC); - - ClientActivateUltravision(client); - - CreateTimer(0.33, Timer_ApplyCustomModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - Call_StartForward(fOnClientSpawnedAsProxy); - Call_PushCell(client); - Call_Finish(); -} - -public Action:Timer_ClientProxyControl(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerProxyControlTimer[client]) return; - - g_iPlayerProxyControl[client]--; - if (g_iPlayerProxyControl[client] <= 0) - { - // ForcePlayerSuicide isn't really dependable, since the player doesn't suicide until several seconds after spawning has passed. - SDKHooks_TakeDamage(client, client, client, 9001.0, DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - return; - } - - g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, userid, TIMER_FLAG_NO_MAPCHANGE); -} - -bool:DoesClientHaveConstantGlow(client) -{ - return g_bPlayerConstantGlowEnabled[client]; -} - -ClientDisableConstantGlow(client) -{ - if (!DoesClientHaveConstantGlow(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientDisableConstantGlow(%d)", client); -#endif - - g_bPlayerConstantGlowEnabled[client] = false; - - new iGlow = EntRefToEntIndex(g_iPlayerConstantGlowEntity[client]); - if (iGlow && iGlow != INVALID_ENT_REFERENCE) AcceptEntityInput(iGlow, "Kill"); - - g_iPlayerConstantGlowEntity[client] = INVALID_ENT_REFERENCE; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientDisableConstantGlow(%d)", client); -#endif -} - -bool:ClientEnableConstantGlow(client, const String:sAttachment[]="") -{ - if (DoesClientHaveConstantGlow(client)) return true; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientEnableConstantGlow(%d)", client); -#endif - - decl String:sModel[PLATFORM_MAX_PATH]; - GetClientModel(client, sModel, sizeof(sModel)); - - if (strlen(sModel) == 0) - { - // For some reason the model couldn't be found, so no. - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false (no model specified)", client); -#endif - - return false; - } - - new iGlow = CreateEntityByName("tf_taunt_prop"); - if (iGlow != -1) - { -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> created"); -#endif - - g_bPlayerConstantGlowEnabled[client] = true; - g_iPlayerConstantGlowEntity[client] = EntIndexToEntRef(iGlow); - - new Float:flModelScale = GetEntPropFloat(client, Prop_Send, "m_flModelScale"); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) - { - DebugMessage("tf_taunt_prop -> get model and model scale (%s, %f, player class: %d)", sModel, flModelScale, TF2_GetPlayerClass(client)); - } -#endif - - SetEntityModel(iGlow, sModel); - DispatchSpawn(iGlow); - ActivateEntity(iGlow); - SetEntityRenderMode(iGlow, RENDER_TRANSCOLOR); - SetEntityRenderColor(iGlow, 0, 0, 0, 0); - SetEntProp(iGlow, Prop_Send, "m_bGlowEnabled", 1); - SetEntPropFloat(iGlow, Prop_Send, "m_flModelScale", flModelScale); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set model and model scale"); -#endif - - // Set effect flags. - new iFlags = GetEntProp(iGlow, Prop_Send, "m_fEffects"); - SetEntProp(iGlow, Prop_Send, "m_fEffects", iFlags | (1 << 0)); // EF_BONEMERGE - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set bonemerge flags"); -#endif - - SetVariantString("!activator"); - AcceptEntityInput(iGlow, "SetParent", client); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent to client"); -#endif - - if (sAttachment[0]) - { - SetVariantString(sAttachment); - AcceptEntityInput(iGlow, "SetParentAttachment"); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent attachment to %s", sAttachment); -#endif - - SDKHook(iGlow, SDKHook_SetTransmit, Hook_ConstantGlowSetTransmit); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> true", client); -#endif - - return true; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false", client); -#endif - - return false; -} - -ClientResetJumpScare(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetJumpScare(%d)", client); -#endif - - g_iPlayerJumpScareBoss[client] = -1; - g_flPlayerJumpScareLifeTime[client] = -1.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetJumpScare(%d)", client); -#endif -} - -ClientDoJumpScare(client, iBossIndex, Float:flLifeTime) -{ - g_iPlayerJumpScareBoss[client] = NPCGetUniqueID(iBossIndex); - g_flPlayerJumpScareLifeTime[client] = GetGameTime() + flLifeTime; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_jumpscare", sBuffer, sizeof(sBuffer), 1); - - if (strlen(sBuffer) > 0) - { - EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN); - } -} - - /** - * Handles sprinting upon player input. - */ -ClientHandleSprint(client, bool:bSprint) -{ - if (!IsPlayerAlive(client) || - g_bPlayerEliminated[client] || - DidClientEscape(client) || - g_bPlayerProxy[client] || - IsClientInGhostMode(client)) return; - - if (bSprint) - { - if (g_iPlayerSprintPoints[client] > 0) - { - ClientStartSprint(client); - } - else - { - EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); - } - } - else - { - if (IsClientSprinting(client)) - { - ClientStopSprint(client); - } - } -} - -ClientOnButtonPress(client, button) -{ - switch (button) - { - case IN_ATTACK2: - { - if (IsPlayerAlive(client)) - { - if (!IsRoundInWarmup() && - !IsRoundInIntro() && - !IsRoundEnding() && - !DidClientEscape(client)) - { - if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) - { - ClientHandleFlashlight(client); - } - } - } - } - case IN_ATTACK3: - { - ClientHandleSprint(client, true); - } - case IN_RELOAD: - { - if (IsPlayerAlive(client)) - { - if (!g_bPlayerEliminated[client]) - { - if (!IsRoundEnding() && - !IsRoundInWarmup() && - !IsRoundInIntro() && - !DidClientEscape(client)) - { - ClientBlink(client); - } - } - } - } - case IN_JUMP: - { - if (IsPlayerAlive(client) && !(GetEntityFlags(client) & FL_FROZEN)) - { - if (!bool:GetEntProp(client, Prop_Send, "m_bDucked") && - (GetEntityFlags(client) & FL_ONGROUND) && - GetEntProp(client, Prop_Send, "m_nWaterLevel") < 2) - { - ClientOnJump(client); - } - } - } - } -} - -ClientOnButtonRelease(client, button) -{ - switch (button) - { - case IN_ATTACK3: - { - ClientHandleSprint(client, false); - } - } -} - -ClientOnJump(client) -{ - if (!g_bPlayerEliminated[client]) - { - if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); - if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) - { - g_iPlayerSprintPoints[client] -= 7; - if (g_iPlayerSprintPoints[client] < 0) g_iPlayerSprintPoints[client] = 0; - } - - if (!IsClientSprinting(client)) - { - if (g_hPlayerSprintTimer[client] == INVALID_HANDLE) - { - // If the player hasn't sprinted recently, force us to regenerate the stamina. - ClientSprintTimer(client, true); - } - } - } - } -} - -// ========================================================== -// DEATH CAM FUNCTIONS -// ========================================================== - -bool:IsClientInDeathCam(client) -{ - return g_bPlayerDeathCam[client]; -} - -public Action:Hook_DeathCamSetTransmit(slender, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - if (EntRefToEntIndex(g_iPlayerDeathCamEnt2[other]) != slender) return Plugin_Handled; - return Plugin_Continue; -} - -ClientResetDeathCam(client) -{ - if (!IsClientInDeathCam(client)) return; // no really need to reset if it wasn't set. - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetDeathCam(%d)", client); -#endif - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - - g_iPlayerDeathCamBoss[client] = -1; - g_bPlayerDeathCam[client] = false; - g_bPlayerDeathCamShowOverlay[client] = false; - g_hPlayerDeathCamTimer[client] = INVALID_HANDLE; - - new ent = EntRefToEntIndex(g_iPlayerDeathCamEnt[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Disable"); - AcceptEntityInput(ent, "Kill"); - } - - ent = EntRefToEntIndex(g_iPlayerDeathCamEnt2[client]); - if (ent && ent != INVALID_ENT_REFERENCE) - { - AcceptEntityInput(ent, "Kill"); - } - - g_iPlayerDeathCamEnt[client] = INVALID_ENT_REFERENCE; - g_iPlayerDeathCamEnt2[client] = INVALID_ENT_REFERENCE; - - if (IsClientInGame(client)) - { - SetClientViewEntity(client, client); - } - - Call_StartForward(fOnClientEndDeathCam); - Call_PushCell(client); - Call_PushCell(iDeathCamBoss); - Call_Finish(); - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetDeathCam(%d)", client); -#endif -} - -ClientStartDeathCam(client, iBossIndex, const Float:vecLookPos[3]) -{ - if (IsClientInDeathCam(client)) return; - if (!NPCIsValid(iBossIndex)) return; - - decl String:buffer[PLATFORM_MAX_PATH]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - if (GetProfileNum(sProfile, "death_cam_play_scare_sound")) - { - GetRandomStringFromProfile(sProfile, "sound_scare_player", buffer, sizeof(buffer)); - if (buffer[0]) EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - - GetRandomStringFromProfile(sProfile, "sound_player_deathcam", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - else - { - // Legacy support for "sound_player_death" - GetRandomStringFromProfile(sProfile, "sound_player_death", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - - GetRandomStringFromProfile(sProfile, "sound_player_deathcam_all", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - else - { - // Legacy support for "sound_player_death_all" - GetRandomStringFromProfile(sProfile, "sound_player_death_all", buffer, sizeof(buffer)); - if (strlen(buffer) > 0) - { - EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); - } - } - - // Call our forward. - Call_StartForward(fOnClientCaughtByBoss); - Call_PushCell(client); - Call_PushCell(iBossIndex); - Call_Finish(); - - if (!NPCHasDeathCamEnabled(iBossIndex)) - { - SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol changes our lifestate. - - // TODO: Add more attributes! - if (NPCHasAttribute(iBossIndex, "ignite player on death")) - { - new Float:flValue = NPCGetAttributeValue(iBossIndex, "ignite player on death"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - - SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - return; - } - - g_iPlayerDeathCamBoss[client] = NPCGetUniqueID(iBossIndex); - g_bPlayerDeathCam[client] = true; - g_bPlayerDeathCamShowOverlay[client] = false; - - decl Float:eyePos[3], Float:eyeAng[3], Float:vecAng[3]; - GetClientEyePosition(client, eyePos); - GetClientEyeAngles(client, eyeAng); - SubtractVectors(eyePos, vecLookPos, vecAng); - GetVectorAngles(vecAng, vecAng); - vecAng[0] = 0.0; - vecAng[2] = 0.0; - - // Create fake model. - new slender = SpawnSlenderModel(iBossIndex, vecLookPos); - TeleportEntity(slender, vecLookPos, vecAng, NULL_VECTOR); - g_iPlayerDeathCamEnt2[client] = EntIndexToEntRef(slender); - SDKHook(slender, SDKHook_SetTransmit, Hook_DeathCamSetTransmit); - - // Create camera look point. - decl String:sName[64]; - Format(sName, sizeof(sName), "sf2_boss_%d", EntIndexToEntRef(slender)); - - decl Float:flOffsetPos[3]; - new target = CreateEntityByName("info_target"); - GetProfileVector(sProfile, "death_cam_pos", flOffsetPos); - AddVectors(vecLookPos, flOffsetPos, flOffsetPos); - TeleportEntity(target, flOffsetPos, NULL_VECTOR, NULL_VECTOR); - DispatchKeyValue(target, "targetname", sName); - SetVariantString("!activator"); - AcceptEntityInput(target, "SetParent", slender); - - // Create the camera itself. - new camera = CreateEntityByName("point_viewcontrol"); - TeleportEntity(camera, eyePos, eyeAng, NULL_VECTOR); - DispatchKeyValue(camera, "spawnflags", "12"); - DispatchKeyValue(camera, "target", sName); - DispatchSpawn(camera); - AcceptEntityInput(camera, "Enable", client); - g_iPlayerDeathCamEnt[client] = EntIndexToEntRef(camera); - - if (GetProfileNum(sProfile, "death_cam_overlay") && GetProfileFloat(sProfile, "death_cam_time_overlay_start") >= 0.0) - { - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_overlay_start"), Timer_ClientResetDeathCam1, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - else - { - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - } - - TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); - - Call_StartForward(fOnClientStartDeathCam); - Call_PushCell(client); - Call_PushCell(iBossIndex); - Call_Finish(); -} - -public Action:Timer_ClientResetDeathCam1(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerDeathCamTimer[client]) return; - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); - - g_bPlayerDeathCamShowOverlay[client] = true; - g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, userid, TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ClientResetDeathCamEnd(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerDeathCamTimer[client]) return; - - SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol entity changes our damage state. - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - if (iDeathCamBoss != -1) - { - if (NPCHasAttribute(iDeathCamBoss, "ignite player on death")) - { - new Float:flValue = NPCGetAttributeValue(iDeathCamBoss, "ignite player on death"); - if (flValue > 0.0) TF2_IgnitePlayer(client, client); - } - } - - SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); - ClientResetDeathCam(client); -} - -// ========================================================== -// GHOST MODE FUNCTIONS -// ========================================================== - -static bool:g_bPlayerGhostMode[MAXPLAYERS + 1] = { false, ... }; -static g_iPlayerGhostModeTarget[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; -static Handle:g_hPlayerGhostModeConnectionCheckTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; -static Float:g_flPlayerGhostModeConnectionTimeOutTime[MAXPLAYERS + 1] = { -1.0, ... }; -static Float:g_flPlayerGhostModeConnectionBootTime[MAXPLAYERS + 1] = { -1.0, ... }; - -/** - * Enables/Disables ghost mode on the player. - */ -ClientSetGhostModeState(client, bool:bState) -{ - if (bState == g_bPlayerGhostMode[client]) return; - - if (bState && !IsClientInGame(client)) return; - - g_bPlayerGhostMode[client] = bState; - g_iPlayerGhostModeTarget[client] = INVALID_ENT_REFERENCE; - - if (bState) - { - ClientHandleGhostMode(client, true); - - if (GetConVarBool(g_cvGhostModeConnectionCheck)) - { - g_hPlayerGhostModeConnectionCheckTimer[client] = CreateTimer(0.0, Timer_GhostModeConnectionCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - } - - PvP_OnClientGhostModeEnable(client); - } - else - { - g_hPlayerGhostModeConnectionCheckTimer[client] = INVALID_HANDLE; - g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - - if (IsClientInGame(client)) - { - TF2_RemoveCondition(client, TFCond_HalloweenGhostMode); - SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_PLAYER); - } - } -} - -public Action:Timer_GhostModeConnectionCheck(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerGhostModeConnectionCheckTimer[client]) return Plugin_Stop; - - if (!IsFakeClient(client) && IsClientTimingOut(client)) - { - new Float:bootTime = g_flPlayerGhostModeConnectionBootTime[client]; - if (bootTime < 0.0) - { - bootTime = GetGameTime() + GetConVarFloat(g_cvGhostModeConnectionTolerance); - g_flPlayerGhostModeConnectionBootTime[client] = bootTime; - g_flPlayerGhostModeConnectionTimeOutTime[client] = GetGameTime(); - } - - if (GetGameTime() >= bootTime) - { - ClientSetGhostModeState(client, false); - TF2_RespawnPlayer(client); - - decl String:authString[128]; - GetClientAuthString(client, authString, sizeof(authString)); - - LogSF2Message("Removed %N (%s) from ghost mode due to timing out for %f seconds", client, authString, GetConVarFloat(g_cvGhostModeConnectionTolerance)); - - new Float:timeOutTime = g_flPlayerGhostModeConnectionTimeOutTime[client]; - CPrintToChat(client, "%T", "SF2 Ghost Mode Bad Connection", client, RoundFloat(bootTime - timeOutTime)); - - return Plugin_Stop; - } - } - else - { - // Player regained connection; reset. - g_flPlayerGhostModeConnectionBootTime[client] = -1.0; - } - - return Plugin_Continue; -} - -/** - * Makes sure that the player is a ghost when ghost mode is activated. - */ -ClientHandleGhostMode(client, bool:bForceSpawn=false) -{ - if (!IsClientInGhostMode(client)) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientHandleGhostMode(%d, %d)", client, bForceSpawn); -#endif - - if (!TF2_IsPlayerInCondition(client, TFCond_HalloweenGhostMode) || bForceSpawn) - { - TF2_AddCondition(client, TFCond_HalloweenGhostMode, -1.0); - SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_DEBRIS); - - // Set first observer target. - ClientGhostModeNextTarget(client); - ClientActivateUltravision(client); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientHandleGhostMode(%d, %d)", client, bForceSpawn); -#endif -} - -ClientGhostModeNextTarget(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientGhostModeNextTarget(%d)", client); -#endif - - new iLastTarget = EntRefToEntIndex(g_iPlayerGhostModeTarget[client]); - new iNextTarget = -1; - new iFirstTarget = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientInGame(i) && (!g_bPlayerEliminated[i] || g_bPlayerProxy[i]) && !IsClientInGhostMode(i) && !DidClientEscape(i) && IsPlayerAlive(i)) - { - if (iFirstTarget == -1) iFirstTarget = i; - if (i > iLastTarget) - { - iNextTarget = i; - break; - } - } - } - - new iTarget = -1; - if (IsValidClient(iNextTarget)) iTarget = iNextTarget; - else iTarget = iFirstTarget; - - if (IsValidClient(iTarget)) - { - g_iPlayerGhostModeTarget[client] = EntIndexToEntRef(iTarget); - - decl Float:flPos[3], Float:flAng[3], Float:flVelocity[3]; - GetClientAbsOrigin(iTarget, flPos); - GetClientEyeAngles(iTarget, flAng); - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsVelocity", flVelocity); - TeleportEntity(client, flPos, flAng, flVelocity); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientGhostModeNextTarget(%d)", client); -#endif -} - -bool:IsClientInGhostMode(client) -{ - return g_bPlayerGhostMode[client]; -} - -// ========================================================== -// SCARE FUNCTIONS -// ========================================================== - -ClientPerformScare(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) - { - LogError("Could not perform scare on client %d: boss does not exist!", client); - return; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - g_flPlayerScareLastTime[client][iBossIndex] = GetGameTime(); - g_flPlayerScareNextTime[client][iBossIndex] = GetGameTime() + NPCGetScareCooldown(iBossIndex); - - // See how much Sanity should be drained from a scare. - new Float:flStaticAmount = GetProfileFloat(sProfile, "scare_static_amount", 0.0); - g_flPlayerStaticAmount[client] += flStaticAmount; - if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; - - decl String:sScareSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_scare_player", sScareSound, sizeof(sScareSound)); - - if (sScareSound[0]) - { - EmitSoundToClient(client, sScareSound, _, MUSIC_CHAN, SNDLEVEL_NONE); - - if (NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS) - { - new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); - new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); - - g_flPlayerSightSoundNextTime[client][iBossIndex] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); - } - - if (g_flPlayerStress[client] > 0.4) - { - ClientAddStress(client, 0.4); - } - else - { - ClientAddStress(client, 0.66); - } - } - else - { - if (g_flPlayerStress[client] > 0.4) - { - ClientAddStress(client, 0.3); - } - else - { - ClientAddStress(client, 0.45); - } - } -} - -ClientPerformSightSound(client, iBossIndex) -{ - if (NPCGetUniqueID(iBossIndex) == -1) - { - LogError("Could not perform sight sound on client %d: boss does not exist!", client); - return; - } - - if (!(NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS)) return; - - new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); - if (iMaster == -1) iMaster = iBossIndex; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sSightSound[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_sight", sSightSound, sizeof(sSightSound)); - - if (sSightSound[0]) - { - EmitSoundToClient(client, sSightSound, _, MUSIC_CHAN, SNDLEVEL_NONE); - - new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); - new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); - - g_flPlayerSightSoundNextTime[client][iMaster] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); - - decl Float:flBossPos[3], Float:flMyPos[3]; - new iBoss = NPCGetEntIndex(iBossIndex); - GetClientAbsOrigin(client, flMyPos); - GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flBossPos); - new Float:flDistUnComfortZone = 400.0; - new Float:flBossDist = GetVectorDistance(flMyPos, flBossPos); - - new Float:flStressScalar = 1.0 + (flDistUnComfortZone / flBossDist); - - ClientAddStress(client, 0.1 * flStressScalar); - } - else - { - LogError("Warning! %s supports sight sounds, but was given a blank sound!", sProfile); - } -} - -ClientResetScare(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetScare(%d)", client); -#endif - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_flPlayerScareNextTime[client][i] = -1.0; - g_flPlayerScareLastTime[client][i] = -1.0; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetScare(%d)", client); -#endif -} - -// ========================================================== -// ANTI-CAMPING FUNCTIONS -// ========================================================== - -stock ClientResetCampingStats(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetCampingStats(%d)", client); -#endif - - g_iPlayerCampingStrikes[client] = 0; - g_hPlayerCampingTimer[client] = INVALID_HANDLE; - g_bPlayerCampingFirstTime[client] = true; - g_flPlayerCampingLastPosition[client][0] = 0.0; - g_flPlayerCampingLastPosition[client][1] = 0.0; - g_flPlayerCampingLastPosition[client][2] = 0.0; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetCampingStats(%d)", client); -#endif -} - -ClientStartCampingTimer(client) -{ - g_hPlayerCampingTimer[client] = CreateTimer(5.0, Timer_ClientCheckCamp, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_ClientCheckCamp(Handle:timer, any:userid) -{ - if (IsRoundInWarmup()) return Plugin_Stop; - - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerCampingTimer[client]) return Plugin_Stop; - - if (IsRoundEnding() || !IsPlayerAlive(client) || g_bPlayerEliminated[client] || DidClientEscape(client)) return Plugin_Stop; - - if (!g_bPlayerCampingFirstTime[client]) - { - decl Float:flPos[3], Float:flMaxs[3], Float:flMins[3]; - GetClientAbsOrigin(client, flPos); - GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); - GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); - - // Only do something if the player is NOT stuck. - new Float:flDistFromLastPosition = GetVectorDistance(g_flPlayerCampingLastPosition[client], flPos); - new Float:flDistFromClosestBoss = 9999999.0; - new iClosestBoss = -1; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - new iSlender = NPCGetEntIndex(i); - if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; - - decl Float:flSlenderPos[3]; - SlenderGetAbsOrigin(i, flSlenderPos); - - new Float:flDist = GetVectorDistance(flSlenderPos, flPos); - if (flDist < flDistFromClosestBoss) - { - iClosestBoss = i; - flDistFromClosestBoss = flDist; - } - } - - if (GetConVarBool(g_cvCampingEnabled) && - !g_bRoundGrace && - !IsSpaceOccupiedIgnorePlayers(flPos, flMins, flMaxs, client) && - g_flPlayerStaticAmount[client] <= GetConVarFloat(g_cvCampingNoStrikeSanity) && - (iClosestBoss == -1 || flDistFromClosestBoss >= GetConVarFloat(g_cvCampingNoStrikeBossDistance)) && - flDistFromLastPosition <= GetConVarFloat(g_cvCampingMinDistance)) - { - g_iPlayerCampingStrikes[client]++; - if (g_iPlayerCampingStrikes[client] < GetConVarInt(g_cvCampingMaxStrikes)) - { - if (g_iPlayerCampingStrikes[client] >= GetConVarInt(g_cvCampingStrikesWarn)) - { - CPrintToChat(client, "{red}%T", "SF2 Camping System Warning", client, (GetConVarInt(g_cvCampingMaxStrikes) - g_iPlayerCampingStrikes[client]) * 5); - } - } - else - { - g_iPlayerCampingStrikes[client] = 0; - ClientStartDeathCam(client, 0, flPos); - } - } - else - { - // Forgiveness. - if (g_iPlayerCampingStrikes[client] > 0) g_iPlayerCampingStrikes[client]--; - } - - g_flPlayerCampingLastPosition[client][0] = flPos[0]; - g_flPlayerCampingLastPosition[client][1] = flPos[1]; - g_flPlayerCampingLastPosition[client][2] = flPos[2]; - } - else - { - g_bPlayerCampingFirstTime[client] = false; - } - - return Plugin_Continue; -} - -// ========================================================== -// BLINK FUNCTIONS -// ========================================================== - -bool:IsClientBlinking(client) -{ - return g_bPlayerBlink[client]; -} - -Float:ClientGetBlinkMeter(client) -{ - return g_flPlayerBlinkMeter[client]; -} - -ClientGetBlinkCount(client) -{ - return g_iPlayerBlinkCount[client]; -} - -/** - * Resets all data on blinking. - */ -ClientResetBlink(client) -{ - g_hPlayerBlinkTimer[client] = INVALID_HANDLE; - g_bPlayerBlink[client] = false; - g_flPlayerBlinkMeter[client] = 1.0; - g_iPlayerBlinkCount[client] = 0; -} - -/** - * Sets the player into a blinking state and blinds the player - */ -ClientBlink(client) -{ - if (IsRoundInWarmup() || DidClientEscape(client)) return; - - if (IsClientBlinking(client)) return; - - g_bPlayerBlink[client] = true; - g_iPlayerBlinkCount[client]++; - g_flPlayerBlinkMeter[client] = 0.0; - g_hPlayerBlinkTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerBlinkHoldTime), Timer_BlinkTimer2, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); - - UTIL_ScreenFade(client, 100, RoundToFloor(GetConVarFloat(g_cvPlayerBlinkHoldTime) * 1000.0), FFADE_IN, 0, 0, 0, 255); - - Call_StartForward(fOnClientBlink); - Call_PushCell(client); - Call_Finish(); -} - -/** - * Unsets the player from the blinking state. - */ -ClientUnblink(client) -{ - if (!IsClientBlinking(client)) return; - - g_bPlayerBlink[client] = false; - g_hPlayerBlinkTimer[client] = INVALID_HANDLE; - g_flPlayerBlinkMeter[client] = 1.0; -} - -ClientStartDrainingBlinkMeter(client) -{ - g_hPlayerBlinkTimer[client] = CreateTimer(ClientGetBlinkRate(client), Timer_BlinkTimer, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); -} - -public Action:Timer_BlinkTimer(Handle:timer, any:userid) -{ - if (IsRoundInWarmup()) return Plugin_Stop; - - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerBlinkTimer[client]) return Plugin_Stop; - - if (IsPlayerAlive(client) && !IsClientInDeathCam(client) && !g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !IsRoundEnding()) - { - new iOverride = GetConVarInt(g_cvPlayerInfiniteBlinkOverride); - if ((!g_bRoundInfiniteBlink && iOverride != 1) || iOverride == 0) - { - g_flPlayerBlinkMeter[client] -= 0.05; - } - - if (g_flPlayerBlinkMeter[client] <= 0.0) - { - ClientBlink(client); - return Plugin_Stop; - } - } - - return Plugin_Continue; -} - -public Action:Timer_BlinkTimer2(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (timer != g_hPlayerBlinkTimer[client]) return; - - ClientUnblink(client); - ClientStartDrainingBlinkMeter(client); -} - -Float:ClientGetBlinkRate(client) -{ - new Float:flValue = GetConVarFloat(g_cvPlayerBlinkRate); - if (GetEntProp(client, Prop_Send, "m_nWaterLevel") >= 3) - { - // Being underwater makes you blink faster, obviously. - flValue *= 0.75; - } - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - if (g_bPlayerSeesSlender[client][i]) - { - flValue *= GetProfileFloat(sProfile, "blink_look_rate_multiply", 1.0); - } - - else if (g_iPlayerStaticMode[client][i] == Static_Increase) - { - flValue *= GetProfileFloat(sProfile, "blink_static_rate_multiply", 1.0); - } - } - - if (TF2_GetPlayerClass(client) == TFClass_Sniper) flValue *= 1.4; - - if (IsClientUsingFlashlight(client)) - { - decl Float:startPos[3], Float:endPos[3], Float:flDirection[3]; - new Float:flLength = SF2_FLASHLIGHT_LENGTH; - GetClientEyePosition(client, startPos); - GetClientEyePosition(client, endPos); - GetClientEyeAngles(client, flDirection); - GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); - NormalizeVector(flDirection, flDirection); - ScaleVector(flDirection, flLength); - AddVectors(endPos, flDirection, endPos); - new Handle:hTrace = TR_TraceRayFilterEx(startPos, endPos, MASK_VISIBLE, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); - TR_GetEndPosition(endPos, hTrace); - new bool:bHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bHit) - { - new Float:flPercent = (GetVectorDistance(startPos, endPos) / flLength); - flPercent *= 3.5; - if (flPercent > 1.0) flPercent = 1.0; - flValue *= flPercent; - } - } - - return flValue; -} - -// ========================================================== -// SCREEN OVERLAY FUNCTIONS -// ========================================================== - -ClientAddStress(client, Float:flStressAmount) -{ - g_flPlayerStress[client] += flStressAmount; - if (g_flPlayerStress[client] < 0.0) g_flPlayerStress[client] = 0.0; - if (g_flPlayerStress[client] > 1.0) g_flPlayerStress[client] = 1.0; - - //PrintCenterText(client, "g_flPlayerStress[%d] = %f", client, g_flPlayerStress[client]); - - SlenderOnClientStressUpdate(client); -} - -stock ClientResetOverlay(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetOverlay(%d)", client); -#endif - - g_hPlayerOverlayCheck[client] = INVALID_HANDLE; - - if (IsClientInGame(client)) - { - ClientCommand(client, "r_screenoverlay \"\""); - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetOverlay(%d)", client); -#endif -} - -public Action:Timer_PlayerOverlayCheck(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerOverlayCheck[client]) return Plugin_Stop; - - if (IsRoundInWarmup()) return Plugin_Continue; - - new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); - new iJumpScareBoss = NPCGetFromUniqueID(g_iPlayerJumpScareBoss[client]); - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - decl String:sMaterial[PLATFORM_MAX_PATH]; - - if (IsClientInDeathCam(client) && iDeathCamBoss != -1 && g_bPlayerDeathCamShowOverlay[client]) - { - NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); - GetRandomStringFromProfile(sProfile, "overlay_player_death", sMaterial, sizeof(sMaterial), 1); - } - else if (iJumpScareBoss != -1 && GetGameTime() <= g_flPlayerJumpScareLifeTime[client]) - { - NPCGetProfile(iJumpScareBoss, sProfile, sizeof(sProfile)); - GetRandomStringFromProfile(sProfile, "overlay_jumpscare", sMaterial, sizeof(sMaterial), 1); - } - else if (IsClientInGhostMode(client)) - { - strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_GHOST); - } - else if (IsRoundInWarmup() || g_bPlayerEliminated[client] || DidClientEscape(client) && !IsClientInGhostMode(client)) - { - return Plugin_Continue; - } - else - { - if (!g_iPlayerPreferences[client][PlayerPreference_FilmGrain]) - strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); - else - strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT); - } - - ClientCommand(client, "r_screenoverlay %s", sMaterial); - return Plugin_Continue; -} - -// ========================================================== -// MUSIC SYSTEM FUNCTIONS -// ========================================================== - -stock ClientUpdateMusicSystem(client, bool:bInitialize=false) -{ - new iOldPageMusicMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); - new iOldMusicFlags = g_iPlayerMusicFlags[client]; - new iChasingBoss = -1; - new iChasingSeeBoss = -1; - new iAlertBoss = -1; - new i20DollarsBoss = -1; - - if (IsRoundEnding() || !IsClientInGame(client) || IsFakeClient(client) || DidClientEscape(client) || (g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !g_bPlayerProxy[client])) - { - g_iPlayerMusicFlags[client] = 0; - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - } - else - { - new bool:bPlayMusicOnEscape = true; - decl String:sName[64]; - new ent = -1; - while ((ent = FindEntityByClassname(ent, "info_target")) != -1) - { - GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); - if (StrEqual(sName, "sf2_escape_custommusic", false)) - { - bPlayMusicOnEscape = false; - break; - } - } - - // Page music first. - new iPageRange = 0; - - if (GetArraySize(g_hPageMusicRanges) > 0) // Map has its own defined page music? - { - for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) - { - ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - new iMin = GetArrayCell(g_hPageMusicRanges, i, 1); - new iMax = GetArrayCell(g_hPageMusicRanges, i, 2); - - if (g_iPageCount >= iMin && g_iPageCount <= iMax) - { - g_iPlayerPageMusicMaster[client] = GetArrayCell(g_hPageMusicRanges, i); - break; - } - } - } - else // Nope. Use old system instead. - { - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - - new Float:flPercent = g_iPageMax > 0 ? (float(g_iPageCount) / float(g_iPageMax)) : 0.0; - if (flPercent > 0.0 && flPercent <= 0.25) iPageRange = 1; - else if (flPercent > 0.25 && flPercent <= 0.5) iPageRange = 2; - else if (flPercent > 0.5 && flPercent <= 0.75) iPageRange = 3; - else if (flPercent > 0.75) iPageRange = 4; - - if (iPageRange == 1) ClientAddMusicFlag(client, MUSICF_PAGES1PERCENT); - else if (iPageRange == 2) ClientAddMusicFlag(client, MUSICF_PAGES25PERCENT); - else if (iPageRange == 3) ClientAddMusicFlag(client, MUSICF_PAGES50PERCENT); - else if (iPageRange == 4) ClientAddMusicFlag(client, MUSICF_PAGES75PERCENT); - } - - if (iPageRange != 1) ClientRemoveMusicFlag(client, MUSICF_PAGES1PERCENT); - if (iPageRange != 2) ClientRemoveMusicFlag(client, MUSICF_PAGES25PERCENT); - if (iPageRange != 3) ClientRemoveMusicFlag(client, MUSICF_PAGES50PERCENT); - if (iPageRange != 4) ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); - - if (IsRoundInEscapeObjective() && !bPlayMusicOnEscape) - { - ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; - } - - new iOldChasingBoss = g_iPlayerChaseMusicMaster[client]; - new iOldChasingSeeBoss = g_iPlayerChaseMusicSeeMaster[client]; - new iOldAlertBoss = g_iPlayerAlertMusicMaster[client]; - new iOld20DollarsBoss = g_iPlayer20DollarsMusicMaster[client]; - - new Float:flAnger = -1.0; - new Float:flSeeAnger = -1.0; - new Float:flAlertAnger = -1.0; - new Float:fl20DollarsAnger = -1.0; - - decl Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - if (NPCGetUniqueID(i) == -1) continue; - - if (NPCGetEntIndex(i) == INVALID_ENT_REFERENCE) continue; - - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - new iBossType = NPCGetType(i); - - switch (iBossType) - { - case SF2BossType_Chaser: - { - GetClientAbsOrigin(client, flBuffer); - SlenderGetAbsOrigin(i, flBuffer3); - - new iTarget = EntRefToEntIndex(g_iSlenderTarget[i]); - if (iTarget != -1) - { - GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBuffer2); - - if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK || g_iSlenderState[i] == STATE_STUN) && - !(NPCGetFlags(i) & SFF_MARKEDASFAKE) && - (iTarget == client || GetVectorDistance(flBuffer, flBuffer2) <= 850.0 || GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0)) - { - decl String:sPath[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sPath, sizeof(sPath), 1); - if (sPath[0]) - { - if (NPCGetAnger(i) > flAnger) - { - flAnger = NPCGetAnger(i); - iChasingBoss = i; - } - } - - if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK) && - PlayerCanSeeSlender(client, i, false)) - { - if (iOldChasingSeeBoss == -1 || !PlayerCanSeeSlender(client, iOldChasingSeeBoss, false) || (NPCGetAnger(i) > flSeeAnger)) - { - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sPath, sizeof(sPath), 1); - - if (sPath[0]) - { - flSeeAnger = NPCGetAnger(i); - iChasingSeeBoss = i; - } - } - - if (g_b20Dollars) - { - if (iOld20DollarsBoss == -1 || !PlayerCanSeeSlender(client, iOld20DollarsBoss, false) || (NPCGetAnger(i) > fl20DollarsAnger)) - { - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sPath, sizeof(sPath), 1); - - if (sPath[0]) - { - fl20DollarsAnger = NPCGetAnger(i); - i20DollarsBoss = i; - } - } - } - } - } - } - - if (g_iSlenderState[i] == STATE_ALERT) - { - decl String:sPath[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sPath, sizeof(sPath), 1); - if (!sPath[0]) continue; - - if (!(NPCGetFlags(i) & SFF_MARKEDASFAKE)) - { - if (GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0) - { - if (NPCGetAnger(i) > flAlertAnger) - { - flAlertAnger = NPCGetAnger(i); - iAlertBoss = i; - } - } - } - } - } - } - } - - if (iChasingBoss != iOldChasingBoss) - { - if (iChasingBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_CHASE); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_CHASE); - } - } - - if (iChasingSeeBoss != iOldChasingSeeBoss) - { - if (iChasingSeeBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_CHASEVISIBLE); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_CHASEVISIBLE); - } - } - - if (iAlertBoss != iOldAlertBoss) - { - if (iAlertBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_ALERT); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_ALERT); - } - } - - if (i20DollarsBoss != iOld20DollarsBoss) - { - if (i20DollarsBoss != -1) - { - ClientAddMusicFlag(client, MUSICF_20DOLLARS); - } - else - { - ClientRemoveMusicFlag(client, MUSICF_20DOLLARS); - } - } - } - - if (IsValidClient(client)) - { - new bool:bWasChase = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASE); - new bool:bChase = ClientHasMusicFlag(client, MUSICF_CHASE); - new bool:bWasChaseSee = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASEVISIBLE); - new bool:bChaseSee = ClientHasMusicFlag(client, MUSICF_CHASEVISIBLE); - new bool:bAlert = ClientHasMusicFlag(client, MUSICF_ALERT); - new bool:bWasAlert = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_ALERT); - new bool:b20Dollars = ClientHasMusicFlag(client, MUSICF_20DOLLARS); - new bool:bWas20Dollars = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_20DOLLARS); - - // Custom system. - if (GetArraySize(g_hPageMusicRanges) > 0) - { - decl String:sPath[PLATFORM_MAX_PATH]; - - new iMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); - if (iMaster != INVALID_ENT_REFERENCE) - { - for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) - { - new ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); - if (!ent || ent == INVALID_ENT_REFERENCE) continue; - - GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - - if (ent == iMaster && - (iOldPageMusicMaster != iMaster || iOldPageMusicMaster == INVALID_ENT_REFERENCE)) - { - if (!sPath[0]) - { - LogError("Could not play music of page range %d-%d: no sound path specified!", GetArrayCell(g_hPageMusicRanges, i, 1), GetArrayCell(g_hPageMusicRanges, i, 2)); - } - else - { - ClientMusicStart(client, sPath, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) - { - GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - StopSound(client, MUSIC_CHAN, sPath); - } - } - } - } - } - else - { - if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) - { - GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); - if (sPath[0]) - { - StopSound(client, MUSIC_CHAN, sPath); - } - } - } - } - - // Old system. - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES1_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES1_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES2_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES2_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES3_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES3_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) - { - StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES4_SOUND); - } - else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) - { - ClientMusicStart(client, MUSIC_GOTPAGES4_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - - new iMainMusicState = 0; - - if (bAlert != bWasAlert || iAlertBoss != g_iPlayerAlertMusicMaster[client]) - { - if (bAlert && !bChase) - { - ClientAlertMusicStart(client, iAlertBoss); - if (!bWasAlert) iMainMusicState = -1; - } - else - { - ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); - if (!bChase && bWasAlert) iMainMusicState = 1; - } - } - - if (bChase != bWasChase || iChasingBoss != g_iPlayerChaseMusicMaster[client]) - { - if (bChase) - { - ClientMusicChaseStart(client, iChasingBoss); - - if (!bWasChase) - { - iMainMusicState = -1; - - if (bAlert) - { - ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); - } - } - } - else - { - ClientMusicChaseStop(client, g_iPlayerChaseMusicMaster[client]); - if (bWasChase) - { - if (bAlert) - { - ClientAlertMusicStart(client, iAlertBoss); - } - else - { - iMainMusicState = 1; - } - } - } - } - - if (bChaseSee != bWasChaseSee || iChasingSeeBoss != g_iPlayerChaseMusicSeeMaster[client]) - { - if (bChaseSee) - { - ClientMusicChaseSeeStart(client, iChasingSeeBoss); - } - else - { - ClientMusicChaseSeeStop(client, g_iPlayerChaseMusicSeeMaster[client]); - } - } - - if (b20Dollars != bWas20Dollars || i20DollarsBoss != g_iPlayer20DollarsMusicMaster[client]) - { - if (b20Dollars) - { - Client20DollarsMusicStart(client, i20DollarsBoss); - } - else - { - Client20DollarsMusicStop(client, g_iPlayer20DollarsMusicMaster[client]); - } - } - - if (iMainMusicState == 1) - { - ClientMusicStart(client, g_strPlayerMusic[client], _, MUSIC_PAGE_VOLUME, bChase || bAlert); - } - else if (iMainMusicState == -1) - { - ClientMusicStop(client); - } - - if (bChase || bAlert) - { - new iBossToUse = -1; - if (bChase) - { - iBossToUse = iChasingBoss; - } - else - { - iBossToUse = iAlertBoss; - } - - if (iBossToUse != -1) - { - // We got some alert/chase music going on! The player's excitement will no doubt go up! - // Excitement, though, really depends on how close the boss is in relation to the - // player. - - new Float:flBossDist = NPCGetDistanceFromEntity(iBossToUse, client); - new Float:flScalar = flBossDist / 700.0 - if (flScalar > 1.0) flScalar = 1.0; - new Float:flStressAdd = 0.1 * (1.0 - flScalar); - - ClientAddStress(client, flStressAdd); - } - } - } -} - -stock ClientMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); - strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerMusicFlags[client] = 0; - g_flPlayerMusicVolume[client] = 0.0; - g_flPlayerMusicTargetVolume[client] = 0.0; - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; -} - -stock ClientMusicStart(client, const String:sNewMusic[], Float:flVolume=-1.0, Float:flTargetVolume=-1.0, bool:bCopyOnly=false) -{ - if (!IsValidClient(client)) return; - if (!sNewMusic[0]) return; - - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); - - if (!StrEqual(sOldMusic, sNewMusic, false)) - { - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - - strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), sNewMusic); - if (flVolume >= 0.0) g_flPlayerMusicVolume[client] = flVolume; - if (flTargetVolume >= 0.0) g_flPlayerMusicTargetVolume[client] = flTargetVolume; - - if (!bCopyOnly) - { - g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeInMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerMusicTimer[client], true); - } - else - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - } -} - -stock ClientMusicStop(client) -{ - g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeOutMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerMusicTimer[client], true); -} - -stock Client20DollarsMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayer20DollarsMusic[client]); - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayer20DollarsMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayer20DollarsMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayer20DollarsMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock Client20DollarsMusicStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayer20DollarsMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayer20DollarsMusicMaster[client] = iBossIndex; - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), sBuffer); - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeIn20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientAlertMusicStop(client, iOldMaster); - } -} - -stock Client20DollarsMusicStop(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayer20DollarsMusicMaster[client]) - { - g_iPlayer20DollarsMusicMaster[client] = -1; - strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); - } - - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOut20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); -} - -stock ClientAlertMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerAlertMusic[client]); - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerAlertMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerAlertMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayerAlertMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_alert_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientAlertMusicStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayerAlertMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayerAlertMusicMaster[client] = iBossIndex; - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), sBuffer); - g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientAlertMusicStop(client, iOldMaster); - } -} - -stock ClientAlertMusicStop(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerAlertMusicMaster[client]) - { - g_iPlayerAlertMusicMaster[client] = -1; - strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); - } - - g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); -} - -stock ClientChaseMusicReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusic[client]); - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); - if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerChaseMusicMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerChaseMusicTimer[client][i] = INVALID_HANDLE; - g_flPlayerChaseMusicVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsValidClient(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_chase_music", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientMusicChaseStart(client, iBossIndex) -{ - if (!IsValidClient(client)) return; - - new iOldMaster = g_iPlayerChaseMusicMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); - - if (!sBuffer[0]) return; - - g_iPlayerChaseMusicMaster[client] = iBossIndex; - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), sBuffer); - g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientMusicChaseStop(client, iOldMaster); - } -} - -stock ClientMusicChaseStop(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerChaseMusicMaster[client]) - { - g_iPlayerChaseMusicMaster[client] = -1; - strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); - } - - g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); -} - -stock ClientChaseMusicSeeReset(client) -{ - new String:sOldMusic[PLATFORM_MAX_PATH]; - strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusicSee[client]); - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); - if (IsClientInGame(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - - g_iPlayerChaseMusicSeeMaster[client] = -1; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_hPlayerChaseMusicSeeTimer[client][i] = INVALID_HANDLE; - g_flPlayerChaseMusicSeeVolumes[client][i] = 0.0; - - if (NPCGetUniqueID(i) != -1) - { - if (IsClientInGame(client)) - { - NPCGetProfile(i, sProfile, sizeof(sProfile)); - - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sOldMusic, sizeof(sOldMusic), 1); - if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); - } - } - } -} - -stock ClientMusicChaseSeeStart(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - - new iOldMaster = g_iPlayerChaseMusicSeeMaster[client]; - if (iOldMaster == iBossIndex) return; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - new String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); - if (!sBuffer[0]) return; - - g_iPlayerChaseMusicSeeMaster[client] = iBossIndex; - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), sBuffer); - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); - - if (iOldMaster != -1) - { - ClientMusicChaseSeeStop(client, iOldMaster); - } -} - -stock ClientMusicChaseSeeStop(client, iBossIndex) -{ - if (!IsClientInGame(client)) return; - if (iBossIndex == -1) return; - - if (iBossIndex == g_iPlayerChaseMusicSeeMaster[client]) - { - g_iPlayerChaseMusicSeeMaster[client] = -1; - strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); - } - - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); - TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); -} - -public Action:Timer_PlayerFadeInMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; - - g_flPlayerMusicVolume[client] += 0.07; - if (g_flPlayerMusicVolume[client] > g_flPlayerMusicTargetVolume[client]) g_flPlayerMusicVolume[client] = g_flPlayerMusicTargetVolume[client]; - - if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); - - if (g_flPlayerMusicVolume[client] >= g_flPlayerMusicTargetVolume[client]) - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; - - g_flPlayerMusicVolume[client] -= 0.07; - if (g_flPlayerMusicVolume[client] < 0.0) g_flPlayerMusicVolume[client] = 0.0; - - if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); - - if (g_flPlayerMusicVolume[client] <= 0.0) - { - g_hPlayerMusicTimer[client] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeIn20DollarsMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayer20DollarsMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayer20DollarsMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] > 1.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayer20DollarsMusic[client][0]) EmitSoundToClient(client, g_strPlayer20DollarsMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); - - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOut20DollarsMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayer20DollarsMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayer20DollarsMusic[client], false)) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayer20DollarsMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] < 0.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); - - if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInAlertMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerAlertMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerAlertMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerAlertMusic[client][0]) EmitSoundToClient(client, g_strPlayerAlertMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); - - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutAlertMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerAlertMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerAlertMusic[client], false)) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerAlertMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); - - if (g_flPlayerAlertMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInChaseMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerChaseMusicVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerChaseMusic[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeInChaseMusicSee(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] += 0.07; - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 1.0; - - if (g_strPlayerChaseMusicSee[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusicSee[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] >= 1.0) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutChaseMusic(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerChaseMusic[client], false)) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerChaseMusicVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -public Action:Timer_PlayerFadeOutChaseMusicSee(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return Plugin_Stop; - - new iBossIndex = -1; - for (new i = 0; i < MAX_BOSSES; i++) - { - if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) - { - iBossIndex = i; - break; - } - } - - if (iBossIndex == -1) return Plugin_Stop; - - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); - - decl String:sBuffer[PLATFORM_MAX_PATH]; - GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); - - if (StrEqual(sBuffer, g_strPlayerChaseMusicSee[client], false)) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] -= 0.07; - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 0.0; - - if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); - - if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] <= 0.0) - { - g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; - return Plugin_Stop; - } - - return Plugin_Continue; -} - -stock bool:ClientHasMusicFlag(client, iFlag) -{ - return bool:(g_iPlayerMusicFlags[client] & iFlag); -} - -stock bool:ClientHasMusicFlag2(iValue, iFlag) -{ - return bool:(iValue & iFlag); -} - -stock ClientAddMusicFlag(client, iFlag) -{ - if (!ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] |= iFlag; -} - -stock ClientRemoveMusicFlag(client, iFlag) -{ - if (ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] &= ~iFlag; -} - -// ========================================================== -// MISC FUNCTIONS -// ========================================================== - -// This could be used for entities as well. -stock ClientStopAllSlenderSounds(client, const String:profileName[], const String:sectionName[], iChannel) -{ - if (!client || !IsValidEntity(client)) return; - - if (!IsProfileValid(profileName)) return; - - decl String:buffer[PLATFORM_MAX_PATH]; - - KvRewind(g_hConfig); - if (KvJumpToKey(g_hConfig, profileName)) - { - decl String:s[32]; - - if (KvJumpToKey(g_hConfig, sectionName)) - { - for (new i2 = 1;; i2++) - { - IntToString(i2, s, sizeof(s)); - KvGetString(g_hConfig, s, buffer, sizeof(buffer)); - if (!buffer[0]) break; - - StopSound(client, iChannel, buffer); - } - } - } -} - -stock ClientUpdateListeningFlags(client, bool:bReset=false) -{ - if (!IsClientInGame(client)) return; - - for (new i = 1; i <= MaxClients; i++) - { - if (i == client || !IsClientInGame(i)) continue; - - if (bReset || IsRoundEnding() || GetConVarBool(g_cvAllChat)) - { - SetListenOverride(client, i, Listen_Default); - continue; - } - - new MuteMode:iMuteMode = g_iPlayerPreferences[client][PlayerPreference_MuteMode]; - - if (g_bPlayerEliminated[client]) - { - if (!g_bPlayerEliminated[i]) - { - if (iMuteMode == MuteMode_DontHearOtherTeam) - { - SetListenOverride(client, i, Listen_No); - } - else if (iMuteMode == MuteMode_DontHearOtherTeamIfNotProxy && !g_bPlayerProxy[client]) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - if (!g_bPlayerEliminated[i]) - { - if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) - { - if (DidClientEscape(i)) - { - if (!DidClientEscape(client)) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - else - { - if (!DidClientEscape(client)) - { - SetListenOverride(client, i, Listen_No); - } - else - { - SetListenOverride(client, i, Listen_Default); - } - } - } - else - { - new bool:bCanHear = false; - if (GetConVarFloat(g_cvPlayerVoiceDistance) <= 0.0) bCanHear = true; - - if (!bCanHear) - { - decl Float:flMyPos[3], Float:flHisPos[3]; - GetClientEyePosition(client, flMyPos); - GetClientEyePosition(i, flHisPos); - - new Float:flDist = GetVectorDistance(flMyPos, flHisPos); - - if (GetConVarFloat(g_cvPlayerVoiceWallScale) > 0.0) - { - new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceRayDontHitCharacters); - new bool:bDidHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bDidHit) - { - flDist *= GetConVarFloat(g_cvPlayerVoiceWallScale); - } - } - - if (flDist <= GetConVarFloat(g_cvPlayerVoiceDistance)) - { - bCanHear = true; - } - } - - if (bCanHear) - { - if (IsClientInGhostMode(i) != IsClientInGhostMode(client) && - DidClientEscape(i) != DidClientEscape(client)) - { - bCanHear = false; - } - } - - if (bCanHear) - { - SetListenOverride(client, i, Listen_Default); - } - else - { - SetListenOverride(client, i, Listen_No); - } - } - } - else - { - SetListenOverride(client, i, Listen_No); - } - } - } -} - -stock ClientShowMainMessage(client, const String:sMessage[], any:...) -{ - decl String:message[512]; - VFormat(message, sizeof(message), sMessage, 3); - - SetHudTextParams(-1.0, 0.4, - 5.0, - 255, - 255, - 255, - 200, - 2, - 1.0, - 0.07, - 2.0); - ShowSyncHudText(client, g_hHudSync, message); -} - -stock ClientResetSlenderStats(client) -{ -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSlenderStats(%d)", client); -#endif - - g_flPlayerStress[client] = 0.0; - g_flPlayerStressNextUpdateTime[client] = -1.0; - - for (new i = 0; i < MAX_BOSSES; i++) - { - g_bPlayerSeesSlender[client][i] = false; - g_flPlayerSeesSlenderLastTime[client][i] = -1.0; - g_flPlayerSightSoundNextTime[client][i] = -1.0; - } - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSlenderStats(%d)", client); -#endif -} - -bool:ClientSetQueuePoints(client, iAmount) -{ - if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return false; - g_iPlayerQueuePoints[client] = iAmount; - ClientSaveCookies(client); - return true; -} - -ClientSaveCookies(client) -{ - if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return; - - // Save and reset our queue points. - decl String:s[64]; - Format(s, sizeof(s), "%d ; %d ; %d ; %d ; %d ; %d", g_iPlayerQueuePoints[client], - g_iPlayerPreferences[client][PlayerPreference_ShowHints], - g_iPlayerPreferences[client][PlayerPreference_MuteMode], - g_iPlayerPreferences[client][PlayerPreference_FilmGrain], - g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection], - g_iPlayerPreferences[client][PlayerPreference_GhostOverlay]); - - SetClientCookie(client, g_hCookie, s); -} - -stock ClientViewPunch(client, const Float:angleOffset[3]) -{ - if (g_offsPlayerPunchAngleVel == -1) return; - - decl Float:flOffset[3]; - for (new i = 0; i < 3; i++) flOffset[i] = angleOffset[i]; - ScaleVector(flOffset, 20.0); - - /* - if (!IsFakeClient(client)) - { - // Latency compensation. - new Float:flLatency = GetClientLatency(client, NetFlow_Outgoing); - new Float:flLatencyCalcDiff = 60.0 * Pow(flLatency, 2.0); - - for (new i = 0; i < 3; i++) flOffset[i] += (flOffset[i] * flLatencyCalcDiff); - } - */ - - decl Float:flAngleVel[3]; - GetEntDataVector(client, g_offsPlayerPunchAngleVel, flAngleVel); - AddVectors(flAngleVel, flOffset, flOffset); - SetEntDataVector(client, g_offsPlayerPunchAngleVel, flOffset, true); -} - -public Action:Hook_ConstantGlowSetTransmit(ent, other) -{ - if (!g_bEnabled) return Plugin_Continue; - - new iOwner = -1; - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (EntRefToEntIndex(g_iPlayerConstantGlowEntity[i]) == ent) - { - iOwner = i; - break; - } - } - - if (iOwner != -1) - { - if (!IsPlayerAlive(iOwner) || g_bPlayerEliminated[iOwner]) return Plugin_Handled; - if (!IsPlayerAlive(other) || (!g_bPlayerProxy[other] && !IsClientInGhostMode(other))) return Plugin_Handled; - } - - return Plugin_Continue; -} - -stock ClientSetFOV(client, iFOV) -{ - SetEntData(client, g_offsPlayerFOV, iFOV); - SetEntData(client, g_offsPlayerDefaultFOV, iFOV); -} - -stock TF2_GetClassName(TFClassType:iClass, String:sBuffer[], sBufferLen) -{ - switch (iClass) - { - case TFClass_Scout: strcopy(sBuffer, sBufferLen, "scout"); - case TFClass_Sniper: strcopy(sBuffer, sBufferLen, "sniper"); - case TFClass_Soldier: strcopy(sBuffer, sBufferLen, "soldier"); - case TFClass_DemoMan: strcopy(sBuffer, sBufferLen, "demoman"); - case TFClass_Heavy: strcopy(sBuffer, sBufferLen, "heavyweapons"); - case TFClass_Medic: strcopy(sBuffer, sBufferLen, "medic"); - case TFClass_Pyro: strcopy(sBuffer, sBufferLen, "pyro"); - case TFClass_Spy: strcopy(sBuffer, sBufferLen, "spy"); - case TFClass_Engineer: strcopy(sBuffer, sBufferLen, "engineer"); - default: strcopy(sBuffer, sBufferLen, ""); - } -} - -#define EF_DIMLIGHT (1 << 2) - -stock ClientSDKFlashlightTurnOn(client) -{ - if (!IsValidClient(client)) return; - - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (iEffects & EF_DIMLIGHT) return; - - iEffects |= EF_DIMLIGHT; - - SetEntProp(client, Prop_Send, "m_fEffects", iEffects); -} - -stock ClientSDKFlashlightTurnOff(client) -{ - if (!IsValidClient(client)) return; - - new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); - if (!(iEffects & EF_DIMLIGHT)) return; - - iEffects &= ~EF_DIMLIGHT; - - SetEntProp(client, Prop_Send, "m_fEffects", iEffects); -} - -stock bool:IsPointVisibleToAPlayer(const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false) -{ - for (new i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (IsPointVisibleToPlayer(i, pos, bCheckFOV, bCheckBlink)) return true; - } - - return false; -} - -stock bool:IsPointVisibleToPlayer(client, const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) -{ - if (!IsValidClient(client) || !IsPlayerAlive(client) || IsClientInGhostMode(client)) return false; - - if (bCheckEliminated && g_bPlayerEliminated[client]) return false; - - if (bCheckBlink && IsClientBlinking(client)) return false; - - decl Float:eyePos[3]; - GetClientEyePosition(client, eyePos); - - // Check fog, if we can. - if (g_offsPlayerFogCtrl != -1 && g_offsFogCtrlEnable != -1 && g_offsFogCtrlEnd != -1) - { - new iFogEntity = GetEntDataEnt2(client, g_offsPlayerFogCtrl); - if (IsValidEdict(iFogEntity)) - { - if (GetEntData(iFogEntity, g_offsFogCtrlEnable) && - GetVectorDistance(eyePos, pos) >= GetEntDataFloat(iFogEntity, g_offsFogCtrlEnd)) - { - return false; - } - } - } - - new Handle:hTrace = TR_TraceRayFilterEx(eyePos, pos, CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); - new bool:bHit = TR_DidHit(hTrace); - CloseHandle(hTrace); - - if (bHit) return false; - - if (bCheckFOV) - { - decl Float:eyeAng[3], Float:reqVisibleAng[3]; - GetClientEyeAngles(client, eyeAng); - - new Float:flFOV = float(g_iPlayerDesiredFOV[client]); - SubtractVectors(pos, eyePos, reqVisibleAng); - GetVectorAngles(reqVisibleAng, reqVisibleAng); - - new Float:difference = FloatAbs(AngleDiff(eyeAng[0], reqVisibleAng[0])) + FloatAbs(AngleDiff(eyeAng[1], reqVisibleAng[1])); - if (difference > ((flFOV * 0.5) + 10.0)) return false; - } - - return true; -} - -public Action:Timer_ClientPostWeapons(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (!IsPlayerAlive(client)) return; - - if (timer != g_hPlayerPostWeaponsTimer[client]) return; - -#if defined DEBUG - if (GetConVarInt(g_cvDebugDetail) > 0) - { - DebugMessage("START Timer_ClientPostWeapons(%d)", client); - } - - new iOldWeaponItemIndexes[6] = { -1, ... }; - new iNewWeaponItemIndexes[6] = { -1, ... }; - - for (new i = 0; i <= 5; i++) - { - new iWeapon = GetPlayerWeaponSlot(client, i); - if (!IsValidEdict(iWeapon)) continue; - - iOldWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - } - -#endif - - new bool:bRemoveWeapons = true; - new bool:bRestrictWeapons = true; - - if (IsRoundEnding()) - { - if (!g_bPlayerEliminated[client]) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - } - - // pvp - if (IsClientInPvP(client)) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - - if (IsRoundInWarmup()) - { - bRemoveWeapons = false; - bRestrictWeapons = false; - } - - if (IsClientInGhostMode(client)) - { - bRemoveWeapons = true; - } - - if (bRemoveWeapons) - { - for (new i = 0; i <= 5; i++) - { - if (i == TFWeaponSlot_Melee && !IsClientInGhostMode(client)) continue; - TF2_RemoveWeaponSlotAndWearables(client, i); - } - - new ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_weapon_builder")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - - ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_wearable_demoshield")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - - ClientSwitchToWeaponSlot(client, TFWeaponSlot_Melee); - } - - if (bRestrictWeapons) - { - new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); - - if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) - { - new TFClassType:iPlayerClass = TF2_GetPlayerClass(client); - new Handle:hItem = INVALID_HANDLE; - - new iWeapon = INVALID_ENT_REFERENCE; - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - - if (IsValidEdict(iWeapon)) - { - if (IsWeaponRestricted(iPlayerClass, GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"))) - { - hItem = INVALID_HANDLE; - TF2_RemoveWeaponSlotAndWearables(client, iSlot); - - switch (iSlot) - { - case TFWeaponSlot_Primary: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponScattergun; - case TFClass_Sniper: hItem = g_hSDKWeaponSniperRifle; - case TFClass_Soldier: hItem = g_hSDKWeaponRocketLauncher; - case TFClass_DemoMan: hItem = g_hSDKWeaponGrenadeLauncher; - case TFClass_Heavy: hItem = g_hSDKWeaponMinigun; - case TFClass_Medic: hItem = g_hSDKWeaponSyringeGun; - case TFClass_Pyro: hItem = g_hSDKWeaponFlamethrower; - case TFClass_Spy: hItem = g_hSDKWeaponRevolver; - case TFClass_Engineer: hItem = g_hSDKWeaponShotgunPrimary; - } - } - case TFWeaponSlot_Secondary: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponPistolScout; - case TFClass_Sniper: hItem = g_hSDKWeaponSMG; - case TFClass_Soldier: hItem = g_hSDKWeaponShotgunSoldier; - case TFClass_DemoMan: hItem = g_hSDKWeaponStickyLauncher; - case TFClass_Heavy: hItem = g_hSDKWeaponShotgunHeavy; - case TFClass_Medic: hItem = g_hSDKWeaponMedigun; - case TFClass_Pyro: hItem = g_hSDKWeaponShotgunPyro; - case TFClass_Engineer: hItem = g_hSDKWeaponPistol; - } - } - case TFWeaponSlot_Melee: - { - switch (iPlayerClass) - { - case TFClass_Scout: hItem = g_hSDKWeaponBat; - case TFClass_Sniper: hItem = g_hSDKWeaponKukri; - case TFClass_Soldier: hItem = g_hSDKWeaponShovel; - case TFClass_DemoMan: hItem = g_hSDKWeaponBottle; - case TFClass_Heavy: hItem = g_hSDKWeaponFists; - case TFClass_Medic: hItem = g_hSDKWeaponBonesaw; - case TFClass_Pyro: hItem = g_hSDKWeaponFireaxe; - case TFClass_Spy: hItem = g_hSDKWeaponKnife; - case TFClass_Engineer: hItem = g_hSDKWeaponWrench; - } - } - case 4: - { - switch (iPlayerClass) - { - case TFClass_Spy: hItem = g_hSDKWeaponInvis; - } - } - } - - if (hItem != INVALID_HANDLE) - { - new iNewWeapon = TF2Items_GiveNamedItem(client, hItem); - if (IsValidEntity(iNewWeapon)) - { - EquipPlayerWeapon(client, iNewWeapon); - } - } - } - } - } - } - - // Fixes the Pretty Boy's Pocket Pistol glitch. - new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, client); - if (iHealth > iMaxHealth) - { - SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); - SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); - } - } - - // Change stats on some weapons. - if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) - { - new iWeapon = INVALID_ENT_REFERENCE; - decl Handle:hWeapon; - for (new iSlot = 0; iSlot <= 5; iSlot++) - { - iWeapon = GetPlayerWeaponSlot(client, iSlot); - if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; - - new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - switch (iItemDef) - { - case 214: // Powerjack - { - TF2_RemoveWeaponSlot(client, iSlot); - - hWeapon = PrepareItemHandle("tf_weapon_fireaxe", 214, 0, 0, "180 ; 20.0 ; 206 ; 1.33"); - new iEnt = TF2Items_GiveNamedItem(client, hWeapon); - CloseHandle(hWeapon); - EquipPlayerWeapon(client, iEnt); - } - } - } - } - - // Remove all hats. - if (IsClientInGhostMode(client)) - { - new ent = -1; - while ((ent = FindEntityByClassname(ent, "tf_wearable")) != -1) - { - if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) - { - AcceptEntityInput(ent, "Kill"); - } - } - } - -#if defined DEBUG - for (new i = 0; i <= 5; i++) - { - new iWeapon = GetPlayerWeaponSlot(client, i); - if (!IsValidEdict(iWeapon)) continue; - - iNewWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); - } - - if (GetConVarInt(g_cvDebugDetail) > 0) - { - for (new i = 0; i <= 5; i++) - { - DebugMessage("-> slot %d: %d (old: %d)", i, iNewWeaponItemIndexes[i], iOldWeaponItemIndexes[i]); - } - - DebugMessage("END Timer_ClientPostWeapons(%d) -> remove = %d, restrict = %d", client, bRemoveWeapons, bRestrictWeapons); - } -#endif -} - -public Action:Timer_ApplyCustomModel(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); - - if (g_bPlayerProxy[client] && iMaster != -1) - { - decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; - NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); - - // Set custom model, if any. - decl String:sBuffer[PLATFORM_MAX_PATH]; - decl String:sSectionName[64]; - - decl String:sClassName[64]; - TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); - - Format(sSectionName, sizeof(sSectionName), "mod_proxy_%s", sClassName); - if ((GetRandomStringFromProfile(sProfile, sSectionName, sBuffer, sizeof(sBuffer)) && sBuffer[0]) || - (GetRandomStringFromProfile(sProfile, "mod_proxy_all", sBuffer, sizeof(sBuffer)) && sBuffer[0])) - { - SetVariantString(sBuffer); - AcceptEntityInput(client, "SetCustomModel"); - SetEntProp(client, Prop_Send, "m_bUseClassAnimations", true); - } - - if (IsPlayerAlive(client)) - { - // Play any sounds, if any. - if (GetRandomStringFromProfile(sProfile, "sound_proxy_spawn", sBuffer, sizeof(sBuffer)) && sBuffer[0]) - { - new iChannel = GetProfileNum(sProfile, "sound_proxy_spawn_channel", SNDCHAN_AUTO); - new iLevel = GetProfileNum(sProfile, "sound_proxy_spawn_level", SNDLEVEL_NORMAL); - new iFlags = GetProfileNum(sProfile, "sound_proxy_spawn_flags", SND_NOFLAGS); - new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_spawn_volume", SNDVOL_NORMAL); - new iPitch = GetProfileNum(sProfile, "sound_proxy_spawn_pitch", SNDPITCH_NORMAL); - - EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); - } - } - } -} - -bool:IsWeaponRestricted(TFClassType:iClass, iItemDef) -{ - if (g_hRestrictedWeaponsConfig == INVALID_HANDLE) return false; - - new bool:bReturn = false; - - decl String:sItemDef[32]; - IntToString(iItemDef, sItemDef, sizeof(sItemDef)); - - KvRewind(g_hRestrictedWeaponsConfig); - if (KvJumpToKey(g_hRestrictedWeaponsConfig, "all")) - { - bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef); - } - - new bool:bFoundSection = false; - KvRewind(g_hRestrictedWeaponsConfig); - - switch (iClass) - { - case TFClass_Scout: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "scout"); - case TFClass_Soldier: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "soldier"); - case TFClass_Sniper: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "sniper"); - case TFClass_DemoMan: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "demoman"); - case TFClass_Heavy: - { - bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavy"); - - if (!bFoundSection) - { - KvRewind(g_hRestrictedWeaponsConfig); - bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavyweapons"); - } - } - case TFClass_Medic: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "medic"); - case TFClass_Spy: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "spy"); - case TFClass_Pyro: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "pyro"); - case TFClass_Engineer: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "engineer"); - } - - if (bFoundSection) - { - bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef, bReturn); - } - - return bReturn; -} - -public Action:Timer_RespawnPlayer(Handle:timer, any:userid) -{ - new client = GetClientOfUserId(userid); - if (client <= 0) return; - - if (IsPlayerAlive(client)) return; - - TF2_RespawnPlayer(client); +#if defined _sf2_client_included + #endinput +#endif +#define _sf2_client_included + +#define GHOST_MODEL "models/props_halloween/ghost_no_hat.mdl" +#define SF2_OVERLAY_DEFAULT "overlays/rytp_horror/grain" +#define SF2_OVERLAY_DEFAULT_NO_FILMGRAIN "overlays/rytp_horror/grain" // TODO: Update material? +#define SF2_OVERLAY_GHOST "overlays/rytp_horror/grain" + +#define EF_NODRAW 32 + +#define SF2_FLASHLIGHT_WIDTH 512.0 // How wide the player's Flashlight should be in world units. +#define SF2_FLASHLIGHT_LENGTH 1024.0 // How far the player's Flashlight can reach in world units. +#define SF2_FLASHLIGHT_BRIGHTNESS 0 // Intensity of the players' Flashlight. +#define SF2_FLASHLIGHT_DRAIN_RATE 0.65 // How long (in seconds) each bar on the player's Flashlight meter lasts. +#define SF2_FLASHLIGHT_RECHARGE_RATE 0.68 // How long (in seconds) it takes each bar on the player's Flashlight meter to recharge. +#define SF2_FLASHLIGHT_FLICKERAT 0.25 // The percentage of the Flashlight battery where the Flashlight will start to blink. +#define SF2_FLASHLIGHT_ENABLEAT 0.3 // The percentage of the Flashlight battery where the Flashlight will be able to be used again (if the player shortens out the Flashlight from excessive use). +#define SF2_FLASHLIGHT_COOLDOWN 0.4 // How much time players have to wait before being able to switch their flashlight on again after turning it off. + +#define SF2_ULTRAVISION_WIDTH 800.0 +#define SF2_ULTRAVISION_LENGTH 800.0 +#define SF2_ULTRAVISION_BRIGHTNESS -4 // Intensity of Ultravision. +#define SF2_ULTRAVISION_CONE 180.0 + +#define SF2_PLAYER_BREATH_COOLDOWN_MIN 0.8 +#define SF2_PLAYER_BREATH_COOLDOWN_MAX 2.0 + +#define SF2_BREATH_VIEWBOB_SPEED 0.05 +#define SF2_BREATH_VIEWBOB_START 0.045 +#define SF2_BREATH_VIEWBOB_AMPLITUDE 0.17 + +new String:g_strPlayerBreathSounds[][] = +{ + "rytp_horror/player_breath_1.wav" +}; + +new String:g_strGhostHelpPhrases[][] = +{ + "rytp_horror/ghost/epifancev_zdes.mp3", + "rytp_horror/ghost/pahom_ohh.mp3", + "rytp_horror/ghost/pahom_ja_nichego_net_net.mp3", + "rytp_horror/ghost/zs_epifan_vou.mp3", + "rytp_horror/ghost/zs_mmm.mp3", + "rytp_horror/ghost/zs_mmm2.mp3" +}; +new g_iGhostHelpPhraseInterval = 2; +new g_iGhostNextHelpPhrase[MAXPLAYERS + 1]; + +static String:g_strPlayerLagCompensationWeapons[][] = +{ + "tf_weapon_sniperrifle", + "tf_weapon_sniperrifle_decap", + "tf_weapon_sniperrifle_classic" +}; + +// Deathcam data. +static g_iPlayerDeathCamBoss[MAXPLAYERS + 1] = { -1, ... }; +static bool:g_bPlayerDeathCam[MAXPLAYERS + 1] = { false, ... }; +static bool:g_bPlayerDeathCamShowOverlay[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerDeathCamEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerDeathCamEnt2[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Handle:g_hPlayerDeathCamTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Flashlight data. +static bool:g_bPlayerFlashlight[MAXPLAYERS + 1] = { false, ... }; +static bool:g_bPlayerFlashlightBroken[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerFlashlightEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerFlashlightEntAng[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Float:g_flPlayerFlashlightBatteryLife[MAXPLAYERS + 1] = { 1.0, ... }; +static Handle:g_hPlayerFlashlightBatteryTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerFlashlightNextInputTime[MAXPLAYERS + 1] = { -1.0, ... }; + +// Ultravision data. +static bool:g_bPlayerUltravision[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerUltravisionEnt[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; + +// Sprint data. +static bool:g_bPlayerSprint[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerSprintPoints[MAXPLAYERS + 1] = { 100, ... }; +static Handle:g_hPlayerSprintTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +// Blink data. +static Handle:g_hPlayerBlinkTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static bool:g_bPlayerBlink[MAXPLAYERS + 1] = { false, ... }; +static Float:g_flPlayerBlinkMeter[MAXPLAYERS + 1] = { 0.0, ... }; +static g_iPlayerBlinkCount[MAXPLAYERS + 1] = { 0, ... }; + +// Breathing data. +static bool:g_bPlayerBreath[MAXPLAYERS + 1] = { false, ... }; +static Handle:g_hPlayerBreathTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; + +new Float:g_flPlayerBreathViewbobPhase[MAXPLAYERS + 1]; +new Float:g_flPlayerBreathViewbobXMult[MAXPLAYERS + 1] = 1.0; +new Float:g_flPlayerBreathViewbobYMult[MAXPLAYERS + 1] = 1.0; + +// Interactive glow data. +static g_iPlayerInteractiveGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static g_iPlayerInteractiveGlowTargetEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; + +// Constant glow data. +static g_iPlayerConstantGlowEntity[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static bool:g_bPlayerConstantGlowEnabled[MAXPLAYERS + 1] = { false, ... }; + +// Jumpscare data. +static g_iPlayerJumpScareBoss[MAXPLAYERS + 1] = { -1, ... }; +static Float:g_flPlayerJumpScareLifeTime[MAXPLAYERS + 1] = { -1.0, ... }; + +static Float:g_flPlayerScareBoostEndTime[MAXPLAYERS + 1] = { -1.0, ... }; + +// Anti-camping data. +static g_iPlayerCampingStrikes[MAXPLAYERS + 1] = { 0, ... }; +static Handle:g_hPlayerCampingTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerCampingLastPosition[MAXPLAYERS + 1][3]; +static bool:g_bPlayerCampingFirstTime[MAXPLAYERS + 1] = { true, ... }; + + +// ========================================================== +// GENERAL CLIENT HOOK FUNCTIONS +// ========================================================== + +#define SF2_PLAYER_VIEWBOB_TIMER 10.0 +#define SF2_PLAYER_VIEWBOB_SCALE_X 0.05 +#define SF2_PLAYER_VIEWBOB_SCALE_Y 0.0 +#define SF2_PLAYER_VIEWBOB_SCALE_Z 0.0 + + +public MRESReturn:Hook_ClientWantsLagCompensationOnEntity(thisPointer, Handle:hReturn, Handle:hParams) +{ + if (!g_bEnabled || IsFakeClient(thisPointer)) return MRES_Ignored; + + DHookSetReturn(hReturn, true); + return MRES_Supercede; +} + +Float:ClientGetScareBoostEndTime(client) +{ + return g_flPlayerScareBoostEndTime[client]; +} + +ClientSetScareBoostEndTime(client, Float:time) +{ + g_flPlayerScareBoostEndTime[client] = time; +} + +public Hook_ClientPreThink(client) +{ + if (!g_bEnabled) return; + + ClientProcessViewAngles(client); + ClientProcessVisibility(client); + ClientProcessStaticShake(client); + ClientProcessFlashlightAngles(client); + ClientProcessInteractiveGlow(client); + + if (IsClientInGhostMode(client)) + { + SetEntPropFloat(client, Prop_Send, "m_flNextAttack", GetGameTime() + 2.0); + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 520.0); + } + else if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) + { + if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) + { + new iRoundState = _:GameRules_GetRoundState(); + + // No double jumping for players in play. + SetEntProp(client, Prop_Send, "m_iAirDash", 99999); + + if (!g_bPlayerProxy[client]) + { + if (iRoundState == 4) + { + new bool:bDanger = false; + + if (!bDanger) + { + decl iState; + decl iBossTarget; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetType(i) == SF2BossType_Chaser) + { + iBossTarget = EntRefToEntIndex(g_iSlenderTarget[i]); + iState = g_iSlenderState[i]; + + if ((iState == STATE_CHASE || iState == STATE_ATTACK || iState == STATE_STUN) && + ((iBossTarget && iBossTarget != INVALID_ENT_REFERENCE && (iBossTarget == client || ClientGetDistanceFromEntity(client, iBossTarget) < 512.0)) || NPCGetDistanceFromEntity(i, client) < 512.0 || PlayerCanSeeSlender(client, i, false))) + { + bDanger = true; + ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); + + // Induce client stress levels. + new Float:flUnComfortZoneDist = 512.0; + new Float:flStressScalar = (flUnComfortZoneDist / NPCGetDistanceFromEntity(i, client)); + ClientAddStress(client, 0.025 * flStressScalar); + + break; + } + } + } + } + + if (g_flPlayerStaticAmount[client] > 0.4) bDanger = true; + if (GetGameTime() < ClientGetScareBoostEndTime(client)) bDanger = true; + + if (!bDanger) + { + decl iState; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetType(i) == SF2BossType_Chaser) + { + if (iState == STATE_ALERT) + { + if (PlayerCanSeeSlender(client, i)) + { + bDanger = true; + ClientSetScareBoostEndTime(client, GetGameTime() + 5.0); + } + } + } + } + } + + if (!bDanger) + { + new Float:flCurTime = GetGameTime(); + new Float:flScareSprintDuration = 3.0; + if (TF2_GetPlayerClass(client) == TFClass_DemoMan) flScareSprintDuration *= 1.667; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if ((flCurTime - g_flPlayerScareLastTime[client][i]) <= flScareSprintDuration) + { + bDanger = true; + break; + } + } + } + + new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); + new Float:flSprintSpeed = ClientGetDefaultSprintSpeed(client); + + // Check for weapon speed changes. + new iWeapon = INVALID_ENT_REFERENCE; + + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; + + new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + switch (iItemDef) + { + case 239: // Gloves of Running Urgently + { + if (GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon") == iWeapon) + { + flSprintSpeed += (flSprintSpeed * 0.1); + } + } + case 775: // Escape Plan + { + new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); + new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); + new Float:flPercentage = flHealth / flMaxHealth; + + if (flPercentage < 0.805 && flPercentage >= 0.605) flSprintSpeed += (flSprintSpeed * 0.05); + else if (flPercentage < 0.605 && flPercentage >= 0.405) flSprintSpeed += (flSprintSpeed * 0.1); + else if (flPercentage < 0.405 && flPercentage >= 0.205) flSprintSpeed += (flSprintSpeed * 0.15); + else if (flPercentage < 0.205) flSprintSpeed += (flSprintSpeed * 0.2); + } + } + } + + // Speed buff? + if (TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly)) + { + flWalkSpeed += (flWalkSpeed * 0.08); + flSprintSpeed += (flSprintSpeed * 0.08); + } + + if (bDanger) + { + flWalkSpeed *= 1.33; + flSprintSpeed *= 1.33; + + if (!g_bPlayerHints[client][PlayerHint_Sprint]) + { + ClientShowHint(client, PlayerHint_Sprint); + } + } + + new Float:flSprintSpeedSubtract = ((flSprintSpeed - flWalkSpeed) * 0.5); + flSprintSpeedSubtract -= flSprintSpeedSubtract * (g_iPlayerSprintPoints[client] != 0 ? (float(g_iPlayerSprintPoints[client]) / 100.0) : 0.0); + flSprintSpeed -= flSprintSpeedSubtract; + + if (IsClientSprinting(client)) + { + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flSprintSpeed); + } + else + { + SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", flWalkSpeed); + } + + if (ClientCanBreath(client) && !g_bPlayerBreath[client]) + { + ClientStartBreathing(client); + } + } + } + else + { + new TFClassType:iClass = TF2_GetPlayerClass(client); + new bool:bSpeedup = TF2_IsPlayerInCondition(client, TFCond_SpeedBuffAlly); + + switch (iClass) + { + case TFClass_Scout: + { + if (iRoundState == 4) + { + if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 405.0); + else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); + } + } + case TFClass_Medic: + { + if (iRoundState == 4) + { + if (bSpeedup) SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 385.0); + else SetEntPropFloat(client, Prop_Send, "m_flMaxspeed", 300.0); + } + } + } + } + } + } + + // Calculate player stress levels. + if (GetGameTime() >= g_flPlayerStressNextUpdateTime[client]) + { + //new Float:flPagePercent = g_iPageMax != 0 ? float(g_iPageCount) / float(g_iPageMax) : 0.0; + //new Float:flPageCountPercent = g_iPageMax != 0? float(g_iPlayerPageCount[client]) / float(g_iPageMax) : 0.0; + + g_flPlayerStressNextUpdateTime[client] = GetGameTime() + 0.33; + ClientAddStress(client, -0.01); + +#if defined DEBUG + SendDebugMessageToPlayer(client, DEBUG_PLAYER_STRESS, 1, "g_flPlayerStress[%d]: %0.1f", client, g_flPlayerStress[client]); +#endif + } + + // Process screen shake, if enabled. + if (g_bPlayerShakeEnabled) + { + new bool:bDoShake = false; + + if (IsPlayerAlive(client)) + { + new iStaticMaster = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); + if (iStaticMaster != -1 && NPCGetFlags(iStaticMaster) & SFF_HASVIEWSHAKE) + { + bDoShake = true; + } + } + + if (bDoShake) + { + new Float:flPercent = g_flPlayerStaticAmount[client]; + + new Float:flAmplitudeMax = GetConVarFloat(g_cvPlayerShakeAmplitudeMax); + new Float:flAmplitude = flAmplitudeMax * flPercent; + + new Float:flFrequencyMax = GetConVarFloat(g_cvPlayerShakeFrequencyMax); + new Float:flFrequency = flFrequencyMax * flPercent; + + UTIL_ScreenShake(client, flAmplitude, 0.5, flFrequency); + } + } +} + +public Action:Hook_ClientSetTransmit(client, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (other != client) + { + if (IsClientInGhostMode(client) && !IsClientInGhostMode(other)) return Plugin_Handled; + + if (!IsRoundEnding()) + { + // SPECIAL ROUND: Singleplayer + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (!g_bPlayerEliminated[client] && !g_bPlayerEliminated[other] && !DidClientEscape(other)) return Plugin_Handled; + } + + // pvp + if (IsClientInPvP(client) && IsClientInPvP(other)) + { + if (TF2_IsPlayerInCondition(client, TFCond_Cloaked) && + !TF2_IsPlayerInCondition(client, TFCond_CloakFlicker) && + !TF2_IsPlayerInCondition(client, TFCond_Jarated) && + !TF2_IsPlayerInCondition(client, TFCond_Milked) && + !TF2_IsPlayerInCondition(client, TFCond_OnFire) && + (GetGameTime() > GetEntPropFloat(client, Prop_Send, "m_flInvisChangeCompleteTime"))) + { + return Plugin_Handled; + } + } + } + } + + return Plugin_Continue; +} + +public Action:TF2_CalcIsAttackCritical(client, weapon, String:sWeaponName[], &bool:result) +{ + if (!g_bEnabled) return Plugin_Continue; + + if ((IsRoundInWarmup() || IsClientInPvP(client)) && !IsRoundEnding()) + { + if (!GetConVarBool(g_cvPlayerFakeLagCompensation)) + { + new bool:bNeedsManualDamage = false; + + // Fake lag compensation isn't enabled; check to see if we need to deal damage manually. + for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) + { + if (StrEqual(sWeaponName, g_strPlayerLagCompensationWeapons[i], false)) + { + bNeedsManualDamage = true; + break; + } + } + + if (bNeedsManualDamage) + { + decl Float:flStartPos[3], Float:flEyeAng[3]; + GetClientEyePosition(client, flStartPos); + GetClientEyeAngles(client, flEyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flEyeAng, MASK_SHOT, RayType_Infinite, TraceRayDontHitEntity, client); + new iHitEntity = TR_GetEntityIndex(hTrace); + new iHitGroup = TR_GetHitGroup(hTrace); + CloseHandle(hTrace); + + if (IsValidClient(iHitEntity)) + { + if (GetClientTeam(iHitEntity) == GetClientTeam(client)) + { + if (IsRoundInWarmup() || IsClientInPvP(iHitEntity)) + { + new Float:flChargedDamage = GetEntPropFloat(weapon, Prop_Send, "m_flChargedDamage"); + if (flChargedDamage < 50.0) flChargedDamage = 50.0; + new iDamageType = DMG_BULLET; + + if (IsClientCritBoosted(client)) + { + result = true; + iDamageType |= DMG_ACID; + } + else if (iHitGroup == 1) + { + if (StrEqual(sWeaponName, "tf_weapon_sniperrifle_classic", false)) + { + if (flChargedDamage >= 150.0) + { + result = true; + iDamageType |= DMG_ACID; + } + } + else + { + if (TF2_IsPlayerInCondition(client, TFCond_Zoomed)) + { + result = true; + iDamageType |= DMG_ACID; + } + } + } + + SDKHooks_TakeDamage(iHitEntity, client, client, flChargedDamage, iDamageType); + return Plugin_Changed; + } + } + } + } + } + } + + return Plugin_Continue; +} + +public Action:Hook_ClientOnTakeDamage(victim, &attacker, &inflictor, &Float:damage, &damagetype, &weapon, Float:damageForce[3], Float:damagePosition[3], damagecustom) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (IsRoundInWarmup()) return Plugin_Continue; + + if (attacker != victim && IsValidClient(attacker)) + { + if (!IsRoundEnding()) + { + if (IsClientInPvP(victim) && IsClientInPvP(attacker)) + { + if (attacker == inflictor) + { + if (IsValidEdict(weapon)) + { + decl String:sWeaponClass[64]; + GetEdictClassname(weapon, sWeaponClass, sizeof(sWeaponClass)); + + // Backstab check! + if ((StrEqual(sWeaponClass, "tf_weapon_knife", false) || (TF2_GetPlayerClass(attacker) == TFClass_Spy && StrEqual(sWeaponClass, "saxxy", false))) && + (damagecustom != TF_CUSTOM_TAUNT_FENCING)) + { + decl Float:flMyPos[3], Float:flHisPos[3], Float:flMyDirection[3]; + GetClientAbsOrigin(victim, flMyPos); + GetClientAbsOrigin(attacker, flHisPos); + GetClientEyeAngles(victim, flMyDirection); + GetAngleVectors(flMyDirection, flMyDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flMyDirection, flMyDirection); + ScaleVector(flMyDirection, 32.0); + AddVectors(flMyDirection, flMyPos, flMyDirection); + + decl Float:p[3], Float:s[3]; + MakeVectorFromPoints(flMyPos, flHisPos, p); + MakeVectorFromPoints(flMyPos, flMyDirection, s); + if (GetVectorDotProduct(p, s) <= 0.0) + { + damage = float(GetEntProp(victim, Prop_Send, "m_iHealth")) * 2.0; + + new Handle:hCvar = FindConVar("tf_weapon_criticals"); + if (hCvar != INVALID_HANDLE && GetConVarBool(hCvar)) damagetype |= DMG_ACID; + return Plugin_Changed; + } + } + } + } + } + /* + else if (g_bPlayerProxy[victim] || g_bPlayerProxy[attacker]) + { + if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) + { + damage = 0.0; + return Plugin_Changed; + } + + if (g_bPlayerProxy[attacker]) + { + new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, victim); + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[attacker]); + if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) + { + if (damagecustom == TF_CUSTOM_TAUNT_GRAND_SLAM || + damagecustom == TF_CUSTOM_TAUNT_FENCING || + damagecustom == TF_CUSTOM_TAUNT_ARROW_STAB || + damagecustom == TF_CUSTOM_TAUNT_GRENADE || + damagecustom == TF_CUSTOM_TAUNT_BARBARIAN_SWING || + damagecustom == TF_CUSTOM_TAUNT_ENGINEER_ARM || + damagecustom == TF_CUSTOM_TAUNT_ARMAGEDDON) + { + if (damage >= float(iMaxHealth)) damage = float(iMaxHealth) * 0.5; + else damage = 0.0; + } + else if (damagecustom == TF_CUSTOM_BACKSTAB) // Modify backstab damage. + { + damage = float(iMaxHealth) * GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy_backstab", 0.25); + if (damagetype & DMG_ACID) damage /= 3.0; + } + + g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitenemy"); + if (g_iPlayerProxyControl[attacker] > 100) + { + g_iPlayerProxyControl[attacker] = 100; + } + + damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_enemy", 1.0); + } + + return Plugin_Changed; + } + else if (g_bPlayerProxy[victim]) + { + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[victim]); + if (iMaster != -1 && g_strSlenderProfile[iMaster][0]) + { + g_iPlayerProxyControl[attacker] += GetProfileNum(g_strSlenderProfile[iMaster], "proxies_controlgain_hitbyenemy"); + if (g_iPlayerProxyControl[attacker] > 100) + { + g_iPlayerProxyControl[attacker] = 100; + } + + damage *= GetProfileFloat(g_strSlenderProfile[iMaster], "proxies_damage_scale_vs_self", 1.0); + } + + return Plugin_Changed; + } + } + */ + else + { + damage = 0.0; + return Plugin_Changed; + } + } + else + { + if (g_bPlayerEliminated[attacker] == g_bPlayerEliminated[victim]) + { + damage = 0.0; + return Plugin_Changed; + } + } + + if (IsClientInGhostMode(victim)) + { + damage = 0.0; + return Plugin_Changed; + } + } + + return Plugin_Continue; +} + +public Action:Hook_TEFireBullets(const String:te_name[], const Players[], numClients, Float:delay) +{ + if (!g_bEnabled) return Plugin_Continue; + + new client = TE_ReadNum("m_iPlayer") + 1; + if (IsValidClient(client)) + { + if (GetConVarBool(g_cvPlayerFakeLagCompensation)) + { + if ((IsRoundInWarmup() || IsClientInPvP(client))) + { + ClientEnableFakeLagCompensation(client); + } + } + } + + return Plugin_Continue; +} + +ClientResetStatic(client) +{ + g_iPlayerStaticMaster[client] = -1; + g_hPlayerStaticTimer[client] = INVALID_HANDLE; + g_flPlayerStaticIncreaseRate[client] = 0.0; + g_flPlayerStaticDecreaseRate[client] = 0.0; + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + g_flPlayerLastStaticTime[client] = 0.0; + g_flPlayerLastStaticVolume[client] = 0.0; + g_bPlayerInStaticShake[client] = false; + g_iPlayerStaticShakeMaster[client] = -1; + g_flPlayerStaticShakeMinVolume[client] = 0.0; + g_flPlayerStaticShakeMaxVolume[client] = 0.0; + g_flPlayerStaticAmount[client] = 0.0; + + if (IsClientInGame(client)) + { + if (g_strPlayerStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticSound[client]); + if (g_strPlayerLastStaticSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + if (g_strPlayerStaticShakeSound[client][0]) StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); + strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), ""); + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); +} + +ClientResetHints(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetHints(%d)", client); +#endif + + for (new i = 0; i < PlayerHint_MaxNum; i++) + { + g_bPlayerHints[client][i] = false; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetHints(%d)", client); +#endif +} + +ClientShowHint(client, iHint) +{ + g_bPlayerHints[client][iHint] = true; + + switch (iHint) + { + case PlayerHint_Sprint: PrintHintText(client, "%T", "SF2 Hint Sprint", client); + case PlayerHint_Flashlight: PrintHintText(client, "%T", "SF2 Hint Flashlight", client); + case PlayerHint_Blink: PrintHintText(client, "%T", "SF2 Hint Blink", client); + case PlayerHint_MainMenu: PrintHintText(client, "%T", "SF2 Hint Main Menu", client); + } +} + +bool:DidClientEscape(client) +{ + return g_bPlayerEscaped[client]; +} + +ClientEscape(client) +{ + if (DidClientEscape(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("START ClientEscape(%d)", client); +#endif + + g_bPlayerEscaped[client] = true; + + ClientResetBreathing(client); + ClientResetSprint(client); + ClientResetFlashlight(client); + ClientDeactivateUltravision(client); + ClientDisableConstantGlow(client); + + // Speed recalculation. Props to the creators of FF2/VSH for this snippet. + TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); + + HandlePlayerHUD(client); + + decl String:sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + CPrintToChatAll("%t", "SF2 Player Escaped", sName); + + CheckRoundWinConditions(); + + Call_StartForward(fOnClientEscape); + Call_PushCell(client); + Call_Finish(); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 1) DebugMessage("END ClientEscape(%d)", client); +#endif +} + +public Action:Timer_TeleportPlayerToEscapePoint(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!DidClientEscape(client)) return; + + if (IsPlayerAlive(client)) + { + TeleportClientToEscapePoint(client); + } +} + +stock Float:ClientGetDistanceFromEntity(client, entity) +{ + decl Float:flStartPos[3], Float:flEndPos[3]; + GetClientAbsOrigin(client, flStartPos); + GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", flEndPos); + return GetVectorDistance(flStartPos, flEndPos); +} + +ClientEnableFakeLagCompensation(client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || g_bPlayerLagCompensation[client]) return; + + // Can only enable lag compensation if we're in either of these two teams only. + new iMyTeam = GetClientTeam(client); + if (iMyTeam != _:TFTeam_Red && iMyTeam != _:TFTeam_Blue) return; + + // Can only enable lag compensation if there are other active teammates around. This is to prevent spontaneous round restarting. + new iCount; + for (new i = 1; i <= MaxClients; i++) + { + if (i == client) continue; + + if (IsValidClient(i) && IsPlayerAlive(i)) + { + new iTeam = GetClientTeam(i); + if ((iTeam == _:TFTeam_Red || iTeam == _:TFTeam_Blue) && iTeam == iMyTeam) + { + iCount++; + } + } + } + + if (!iCount) return; + + // Can only enable lag compensation only for specific weapons. + new iActiveWeapon = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + if (!IsValidEdict(iActiveWeapon)) return; + + decl String:sClassName[64]; + GetEdictClassname(iActiveWeapon, sClassName, sizeof(sClassName)); + + new bool:bCompensate = false; + for (new i = 0; i < sizeof(g_strPlayerLagCompensationWeapons); i++) + { + if (StrEqual(sClassName, g_strPlayerLagCompensationWeapons[i], false)) + { + bCompensate = true; + break; + } + } + + if (!bCompensate) return; + + g_bPlayerLagCompensation[client] = true; + g_iPlayerLagCompensationTeam[client] = iMyTeam; + SetEntProp(client, Prop_Send, "m_iTeamNum", 0); +} + +ClientDisableFakeLagCompensation(client) +{ + if (!g_bPlayerLagCompensation[client]) return; + + SetEntProp(client, Prop_Send, "m_iTeamNum", g_iPlayerLagCompensationTeam[client]); + g_bPlayerLagCompensation[client] = false; + g_iPlayerLagCompensationTeam[client] = -1; +} + +// ========================================================== +// FLASHLIGHT / ULTRAVISION FUNCTIONS +// ========================================================== + +bool:IsClientUsingFlashlight(client) +{ + return g_bPlayerFlashlight[client]; +} + +Float:ClientGetFlashlightBatteryLife(client) +{ + return g_flPlayerFlashlightBatteryLife[client]; +} + +ClientSetFlashlightBatteryLife(client, Float:flPercent) +{ + g_flPlayerFlashlightBatteryLife[client] = flPercent; +} + +/** + * Called in Hook_ClientPreThink, this makes sure the flashlight is oriented correctly on the player. + */ +static ClientProcessFlashlightAngles(client) +{ + if (!IsClientInGame(client)) return; + + if (IsPlayerAlive(client)) + { + decl fl, Float:eyeAng[3], Float:ang2[3]; + + if (IsClientUsingFlashlight(client)) + { + fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + TeleportEntity(fl, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }, NULL_VECTOR); + } + + fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + GetClientEyeAngles(client, eyeAng); + GetClientAbsAngles(client, ang2); + SubtractVectors(eyeAng, ang2, eyeAng); + TeleportEntity(fl, NULL_VECTOR, eyeAng, NULL_VECTOR); + } + } + } +} + +/** + * Handles whether or not the player's flashlight should be "flickering", a sign of a dying flashlight battery. + */ +static ClientHandleFlashlightFlickerState(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) + { + new bool:bFlicker = bool:(ClientGetFlashlightBatteryLife(client) <= SF2_FLASHLIGHT_FLICKERAT); + + new fl = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + if (bFlicker) + { + SetEntProp(fl, Prop_Data, "m_LightStyle", 10); + } + else + { + SetEntProp(fl, Prop_Data, "m_LightStyle", 0); + } + } + + fl = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (fl && fl != INVALID_ENT_REFERENCE) + { + if (bFlicker) + { + SetEntityRenderFx(fl, RenderFx:13); + } + else + { + SetEntityRenderFx(fl, RenderFx:0); + } + } + } +} + +bool:IsClientFlashlightBroken(client) +{ + return g_bPlayerFlashlightBroken[client]; +} + +Float:ClientGetFlashlightNextInputTime(client) +{ + return g_flPlayerFlashlightNextInputTime[client]; +} + +/** + * Breaks the player's flashlight. Nothing else. + */ +ClientBreakFlashlight(client) +{ + if (IsClientFlashlightBroken(client)) return; + + g_bPlayerFlashlightBroken[client] = true; + + ClientSetFlashlightBatteryLife(client, 0.0); + ClientTurnOffFlashlight(client); + + ClientAddStress(client, 0.2); + + EmitSoundToAll(FLASHLIGHT_BREAKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + + Call_StartForward(fOnClientBreakFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Resets everything of the player's flashlight. + */ +ClientResetFlashlight(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetFlashlight(%d)", client); +#endif + + ClientTurnOffFlashlight(client); + ClientSetFlashlightBatteryLife(client, 1.0); + g_bPlayerFlashlightBroken[client] = false; + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + g_flPlayerFlashlightNextInputTime[client] = -1.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetFlashlight(%d)", client); +#endif +} + +public Action:Hook_FlashlightSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEnt[other]) != ent) return Plugin_Handled; + + // We've already checked for flashlight ownership in the last statement. So we can do just this. + if (g_iPlayerPreferences[other][PlayerPreference_ProjectedFlashlight]) return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:Hook_Flashlight2SetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[other]) == ent) return Plugin_Handled; + return Plugin_Continue; +} + +public Hook_FlashlightEndSpawnPost(ent) +{ + if (!g_bEnabled) return; + + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightEndSetTransmit); + SDKUnhook(ent, SDKHook_SpawnPost, Hook_FlashlightEndSpawnPost); +} + +public Action:Hook_FlashlightBeamSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + new iSpotlight = -1; + while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) + { + if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) + { + iOwner = iSpotlight; + break; + } + } + + if (iOwner == -1) return Plugin_Continue; + + new iClient = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) + { + iClient = i; + break; + } + } + + if (iClient == -1) return Plugin_Continue; + + if (iClient == other) + { + if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) + { + return Plugin_Handled; + } + } + else + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Hook_FlashlightEndSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + new iSpotlight = -1; + while ((iSpotlight = FindEntityByClassname(iSpotlight, "point_spotlight")) != -1) + { + if (GetEntPropEnt(ent, Prop_Data, "m_hOwnerEntity") == iSpotlight) + { + iOwner = iSpotlight; + break; + } + } + + if (iOwner == -1) return Plugin_Continue; + + new iClient = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + + if (EntRefToEntIndex(g_iPlayerFlashlightEntAng[i]) == iOwner) + { + iClient = i; + break; + } + } + + if (iClient == -1) return Plugin_Continue; + + if (iClient == other) + { + if (!GetEntProp(iClient, Prop_Send, "m_nForceTauntCam") || !GetEntProp(iClient, Prop_Send, "m_iObserverMode")) + { + return Plugin_Handled; + } + } + else + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action:Timer_DrainFlashlight(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; + + new iOverride = GetConVarInt(g_cvPlayerInfiniteFlashlightOverride); + if ((!g_bRoundInfiniteFlashlight && iOverride != 1) || iOverride == 0) + { + ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) - 0.01); + } + + if (ClientGetFlashlightBatteryLife(client) <= 0.0) + { + // Break the player's flashlight, but also start recharging. + ClientBreakFlashlight(client); + ClientStartRechargingFlashlightBattery(client); + ClientActivateUltravision(client); + return Plugin_Stop; + } + else + { + ClientHandleFlashlightFlickerState(client); + } + + return Plugin_Continue; +} + +public Action:Timer_RechargeFlashlight(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerFlashlightBatteryTimer[client]) return Plugin_Stop; + + ClientSetFlashlightBatteryLife(client, ClientGetFlashlightBatteryLife(client) + 0.01); + + if (IsClientFlashlightBroken(client) && ClientGetFlashlightBatteryLife(client) >= SF2_FLASHLIGHT_ENABLEAT) + { + // Repair the flashlight. + g_bPlayerFlashlightBroken[client] = false; + } + + if (ClientGetFlashlightBatteryLife(client) >= 1.0) + { + // I am fully charged! + ClientSetFlashlightBatteryLife(client, 1.0); + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + + return Plugin_Stop; + } + + return Plugin_Continue; +} + +/** + * Turns on the player's flashlight. Nothing else. + */ +ClientTurnOnFlashlight(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) return; + + g_bPlayerFlashlight[client] = true; + + decl Float:flEyePos[3]; + GetClientEyePosition(client, flEyePos); + + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + // If the player is using the projected flashlight, just set effect flags. + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (!(iEffects & (1 << 2))) + { + SetEntProp(client, Prop_Send, "m_fEffects", iEffects | (1 << 2)); + } + } + else + { + // Spawn the light which only the user will see. + new ent = CreateEntityByName("light_dynamic"); + if (ent != -1) + { + TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); + DispatchKeyValue(ent, "targetname", "WUBADUBDUBMOTHERBUCKERS"); + DispatchKeyValue(ent, "rendercolor", "255 255 255"); + SetVariantFloat(SF2_FLASHLIGHT_WIDTH); + AcceptEntityInput(ent, "spotlight_radius"); + SetVariantFloat(SF2_FLASHLIGHT_LENGTH); + AcceptEntityInput(ent, "distance"); + SetVariantInt(SF2_FLASHLIGHT_BRIGHTNESS); + AcceptEntityInput(ent, "brightness"); + + // Convert WU to inches. + new Float:cone = 55.0; + cone *= 0.75; + + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_inner_cone"); + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_cone"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "TurnOn"); + + g_iPlayerFlashlightEnt[client] = EntIndexToEntRef(ent); + + SDKHook(ent, SDKHook_SetTransmit, Hook_FlashlightSetTransmit); + } + } + + // Spawn the light that only everyone else will see. + new ent = CreateEntityByName("point_spotlight"); + if (ent != -1) + { + TeleportEntity(ent, flEyePos, NULL_VECTOR, NULL_VECTOR); + + decl String:sBuffer[256]; + FloatToString(SF2_FLASHLIGHT_LENGTH, sBuffer, sizeof(sBuffer)); + DispatchKeyValue(ent, "spotlightlength", sBuffer); + FloatToString(SF2_FLASHLIGHT_WIDTH, sBuffer, sizeof(sBuffer)); + DispatchKeyValue(ent, "spotlightwidth", sBuffer); + DispatchKeyValue(ent, "rendercolor", "255 255 255"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "LightOn"); + + g_iPlayerFlashlightEntAng[client] = EntIndexToEntRef(ent); + } + + Call_StartForward(fOnClientActivateFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Turns off the player's flashlight. Nothing else. + */ +ClientTurnOffFlashlight(client) +{ + if (!IsClientUsingFlashlight(client)) return; + + g_bPlayerFlashlight[client] = false; + g_hPlayerFlashlightBatteryTimer[client] = INVALID_HANDLE; + + // Remove user-only light. + new ent = EntRefToEntIndex(g_iPlayerFlashlightEnt[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "TurnOff"); + AcceptEntityInput(ent, "Kill"); + } + + // Remove everyone-else-only light. + ent = EntRefToEntIndex(g_iPlayerFlashlightEntAng[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "LightOff"); + CreateTimer(0.1, Timer_KillEntity, g_iPlayerFlashlightEntAng[client], TIMER_FLAG_NO_MAPCHANGE); + } + + g_iPlayerFlashlightEnt[client] = INVALID_ENT_REFERENCE; + g_iPlayerFlashlightEntAng[client] = INVALID_ENT_REFERENCE; + + if (IsClientInGame(client)) + { + if (g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight]) + { + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (iEffects & (1 << 2)) + { + SetEntProp(client, Prop_Send, "m_fEffects", iEffects &= ~(1 << 2)); + } + } + } + + Call_StartForward(fOnClientDeactivateFlashlight); + Call_PushCell(client); + Call_Finish(); +} + +ClientStartRechargingFlashlightBattery(client) +{ + g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(SF2_FLASHLIGHT_RECHARGE_RATE, Timer_RechargeFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStartDrainingFlashlightBattery(client) +{ + new Float:flDrainRate = SF2_FLASHLIGHT_DRAIN_RATE; + if (TF2_GetPlayerClass(client) == TFClass_Engineer) + { + // Engineers have a 33% longer battery life, basically. + // TODO: Make this value customizable via cvar. + flDrainRate *= 1.33; + } + + g_hPlayerFlashlightBatteryTimer[client] = CreateTimer(flDrainRate, Timer_DrainFlashlight, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +ClientHandleFlashlight(client) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client)) return; + + if (IsClientUsingFlashlight(client)) + { + ClientTurnOffFlashlight(client); + ClientStartRechargingFlashlightBattery(client); + ClientActivateUltravision(client); + + g_flPlayerFlashlightNextInputTime[client] = GetGameTime() + SF2_FLASHLIGHT_COOLDOWN; + + EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + } + else + { + // Only players in the "game" can use the flashlight. + if (!g_bPlayerEliminated[client]) + { + new bool:bCanUseFlashlight = true; + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_LIGHTSOUT) + { + // Unequip the flashlight please. + bCanUseFlashlight = false; + } + + if (!IsClientFlashlightBroken(client) && bCanUseFlashlight) + { + ClientTurnOnFlashlight(client); + ClientStartDrainingFlashlightBattery(client); + ClientDeactivateUltravision(client); + + g_flPlayerFlashlightNextInputTime[client] = GetGameTime(); + + EmitSoundToAll(FLASHLIGHT_CLICKSOUND, client, SNDCHAN_STATIC, SNDLEVEL_DRYER); + } + else + { + EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); + } + } + } +} + +bool:IsClientUsingUltravision(client) +{ + return g_bPlayerUltravision[client]; +} + +ClientActivateUltravision(client) +{ + if (!IsClientInGame(client) || IsClientUsingUltravision(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientActivateUltravision(%d)", client); +#endif + + g_bPlayerUltravision[client] = true; + + new ent = CreateEntityByName("light_dynamic"); + if (ent != -1) + { + decl Float:flEyePos[3]; + GetClientEyePosition(client, flEyePos); + + TeleportEntity(ent, flEyePos, Float:{ 90.0, 0.0, 0.0 }, NULL_VECTOR); + DispatchKeyValue(ent, "rendercolor", "0 200 255"); + + new Float:flRadius = 0.0; + if (g_bPlayerEliminated[client]) + { + flRadius = GetConVarFloat(g_cvUltravisionRadiusBlue); + } + else + { + flRadius = GetConVarFloat(g_cvUltravisionRadiusRed); + } + + SetVariantFloat(flRadius); + AcceptEntityInput(ent, "spotlight_radius"); + SetVariantFloat(flRadius); + AcceptEntityInput(ent, "distance"); + + SetVariantInt(-15); // Start dark, then fade in via the Timer_UltravisionFadeInEffect timer func. + AcceptEntityInput(ent, "brightness"); + + // Convert WU to inches. + new Float:cone = SF2_ULTRAVISION_CONE; + cone *= 0.75; + + SetVariantInt(RoundToFloor(cone)); + AcceptEntityInput(ent, "_inner_cone"); + SetVariantInt(0); + AcceptEntityInput(ent, "_cone"); + DispatchSpawn(ent); + ActivateEntity(ent); + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", client); + AcceptEntityInput(ent, "TurnOn"); + SetEntityRenderFx(ent, RENDERFX_SOLID_SLOW); + SetEntityRenderColor(ent, 100, 200, 255, 255); + + g_iPlayerUltravisionEnt[client] = EntIndexToEntRef(ent); + + SDKHook(ent, SDKHook_SetTransmit, Hook_UltravisionSetTransmit); + + // Fade in effect. + CreateTimer(0.0, Timer_UltravisionFadeInEffect, g_iPlayerUltravisionEnt[client], TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientActivateUltravision(%d)", client); +#endif +} + +public Action:Timer_UltravisionFadeInEffect(Handle:timer, any:entref) +{ + new ent = EntRefToEntIndex(entref); + if (!ent || ent == INVALID_ENT_REFERENCE) return Plugin_Stop; + + new iBrightness = GetEntProp(ent, Prop_Send, "m_Exponent"); + if (iBrightness >= GetConVarInt(g_cvUltravisionBrightness)) return Plugin_Stop; + + iBrightness++; + SetVariantInt(iBrightness); + AcceptEntityInput(ent, "brightness"); + + return Plugin_Continue; +} + +ClientDeactivateUltravision(client) +{ + if (!IsClientUsingUltravision(client)) return; + + g_bPlayerUltravision[client] = false; + + new ent = EntRefToEntIndex(g_iPlayerUltravisionEnt[client]); + if (ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "TurnOff"); + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerUltravisionEnt[client] = INVALID_ENT_REFERENCE; +} + +public Action:Hook_UltravisionSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (!GetConVarBool(g_cvUltravisionEnabled) || EntRefToEntIndex(g_iPlayerUltravisionEnt[other]) != ent || !IsPlayerAlive(other)) return Plugin_Handled; + return Plugin_Continue; +} + +static Float:ClientGetDefaultWalkSpeed(client) +{ + new Float:flReturn = 190.0; + new Float:flReturn2 = flReturn; + new Action:iAction = Plugin_Continue; + new TFClassType:iClass = TF2_GetPlayerClass(client); + + switch (iClass) + { + case TFClass_Scout: flReturn = 190.0; + case TFClass_Sniper: flReturn = 190.0; + case TFClass_Soldier: flReturn = 190.0; + case TFClass_DemoMan: flReturn = 190.0; + case TFClass_Heavy: flReturn = 190.0; + case TFClass_Medic: flReturn = 190.0; + case TFClass_Pyro: flReturn = 190.0; + case TFClass_Spy: flReturn = 190.0; + case TFClass_Engineer: flReturn = 190.0; + } + + // Call our forward. + Call_StartForward(fOnClientGetDefaultWalkSpeed); + Call_PushCell(client); + Call_PushCellRef(flReturn2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) flReturn = flReturn2; + + return flReturn; +} + +static Float:ClientGetDefaultSprintSpeed(client) +{ + new Float:flReturn = 300.0; + new Float:flReturn2 = flReturn; + new Action:iAction = Plugin_Continue; + new TFClassType:iClass = TF2_GetPlayerClass(client); + + switch (iClass) + { + case TFClass_Scout: flReturn = 300.0; + case TFClass_Sniper: flReturn = 300.0; + case TFClass_Soldier: flReturn = 275.0; + case TFClass_DemoMan: flReturn = 285.0; + case TFClass_Heavy: flReturn = 270.0; + case TFClass_Medic: flReturn = 300.0; + case TFClass_Pyro: flReturn = 300.0; + case TFClass_Spy: flReturn = 300.0; + case TFClass_Engineer: flReturn = 300.0; + } + + // Call our forward. + Call_StartForward(fOnClientGetDefaultSprintSpeed); + Call_PushCell(client); + Call_PushCellRef(flReturn2); + Call_Finish(iAction); + + if (iAction == Plugin_Changed) flReturn = flReturn2; + + return flReturn; +} + +// Static shaking should only affect the x, y portion of the player's view, not roll. +// This is purely for cosmetic effect. + +ClientProcessStaticShake(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + new bool:bOldStaticShake = g_bPlayerInStaticShake[client]; + new iOldStaticShakeMaster = NPCGetFromUniqueID(g_iPlayerStaticShakeMaster[client]); + new iNewStaticShakeMaster = -1; + new Float:flNewStaticShakeMasterAnger = -1.0; + + new Float:flOldPunchAng[3], Float:flOldPunchAngVel[3]; + GetEntDataVector(client, g_offsPlayerPunchAngle, flOldPunchAng); + GetEntDataVector(client, g_offsPlayerPunchAngleVel, flOldPunchAngVel); + + new Float:flNewPunchAng[3], Float:flNewPunchAngVel[3]; + + for (new i = 0; i < 3; i++) + { + flNewPunchAng[i] = flOldPunchAng[i]; + flNewPunchAngVel[i] = flOldPunchAngVel[i]; + } + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (g_iPlayerStaticMode[client][i] != Static_Increase) continue; + if (!(NPCGetFlags(i) & SFF_HASSTATICSHAKE)) continue; + + if (NPCGetAnger(i) > flNewStaticShakeMasterAnger) + { + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + if (iMaster == -1) iMaster = i; + + iNewStaticShakeMaster = iMaster; + flNewStaticShakeMasterAnger = NPCGetAnger(iMaster); + } + } + + if (iNewStaticShakeMaster != -1) + { + g_iPlayerStaticShakeMaster[client] = NPCGetUniqueID(iNewStaticShakeMaster); + + if (iNewStaticShakeMaster != iOldStaticShakeMaster) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iNewStaticShakeMaster, sProfile, sizeof(sProfile)); + + if (g_strPlayerStaticShakeSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + g_flPlayerStaticShakeMinVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_min", 0.0); + g_flPlayerStaticShakeMaxVolume[client] = GetProfileFloat(sProfile, "sound_static_shake_local_volume_max", 1.0); + + decl String:sStaticSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_shake_local", sStaticSound, sizeof(sStaticSound)); + if (sStaticSound[0]) + { + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), sStaticSound); + } + else + { + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); + } + } + } + + if (g_bPlayerInStaticShake[client]) + { + if (g_flPlayerStaticAmount[client] <= 0.0) + { + g_bPlayerInStaticShake[client] = false; + } + } + else + { + if (iNewStaticShakeMaster != -1) + { + g_bPlayerInStaticShake[client] = true; + } + } + + if (g_bPlayerInStaticShake[client] && !bOldStaticShake) + { + for (new i = 0; i < 2; i++) + { + flNewPunchAng[i] = 0.0; + flNewPunchAngVel[i] = 0.0; + } + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } + else if (!g_bPlayerInStaticShake[client] && bOldStaticShake) + { + for (new i = 0; i < 2; i++) + { + flNewPunchAng[i] = 0.0; + flNewPunchAngVel[i] = 0.0; + } + + g_iPlayerStaticShakeMaster[client] = -1; + + if (g_strPlayerStaticShakeSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerStaticShakeSound[client]); + } + + strcopy(g_strPlayerStaticShakeSound[client], sizeof(g_strPlayerStaticShakeSound[]), ""); + + g_flPlayerStaticShakeMinVolume[client] = 0.0; + g_flPlayerStaticShakeMaxVolume[client] = 0.0; + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } + + if (g_bPlayerInStaticShake[client]) + { + if (g_strPlayerStaticShakeSound[client][0]) + { + new Float:flVolume = g_flPlayerStaticAmount[client]; + if (GetRandomFloat(0.0, 1.0) <= 0.35) + { + flVolume = 0.0; + } + else + { + if (flVolume < g_flPlayerStaticShakeMinVolume[client]) + { + flVolume = g_flPlayerStaticShakeMinVolume[client]; + } + + if (flVolume > g_flPlayerStaticShakeMaxVolume[client]) + { + flVolume = g_flPlayerStaticShakeMaxVolume[client]; + } + } + + EmitSoundToClient(client, g_strPlayerStaticShakeSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL | SND_STOP, flVolume); + } + + // Spazz our view all over the place. + for (new i = 0; i < 2; i++) flNewPunchAng[i] = AngleNormalize(GetRandomFloat(0.0, 360.0)); + NormalizeVector(flNewPunchAng, flNewPunchAng); + + new Float:flAngVelocityScalar = 5.0 * g_flPlayerStaticAmount[client]; + if (flAngVelocityScalar < 1.0) flAngVelocityScalar = 1.0; + ScaleVector(flNewPunchAng, flAngVelocityScalar); + + for (new i = 0; i < 2; i++) flNewPunchAngVel[i] = 0.0; + + SetEntDataVector(client, g_offsPlayerPunchAngle, flNewPunchAng, true); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flNewPunchAngVel, true); + } +} + +ClientProcessVisibility(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client)) return; + + new String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + new bool:bWasSeeingSlender[MAX_BOSSES]; + new iOldStaticMode[MAX_BOSSES]; + + decl Float:flSlenderPos[3]; + decl Float:flSlenderEyePos[3]; + decl Float:flSlenderOBBCenterPos[3]; + + decl Float:flMyPos[3]; + GetClientAbsOrigin(client, flMyPos); + + for (new i = 0; i < MAX_BOSSES; i++) + { + bWasSeeingSlender[i] = g_bPlayerSeesSlender[client][i]; + iOldStaticMode[i] = g_iPlayerStaticMode[client][i]; + g_bPlayerSeesSlender[client][i] = false; + g_iPlayerStaticMode[client][i] = Static_None; + + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + new iBoss = NPCGetEntIndex(i); + + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + SlenderGetAbsOrigin(i, flSlenderPos); + NPCGetEyePosition(i, flSlenderEyePos); + + decl Float:flSlenderMins[3], Float:flSlenderMaxs[3]; + GetEntPropVector(iBoss, Prop_Send, "m_vecMins", flSlenderMins); + GetEntPropVector(iBoss, Prop_Send, "m_vecMaxs", flSlenderMaxs); + + for (new i2 = 0; i2 < 3; i2++) flSlenderOBBCenterPos[i2] = flSlenderPos[i2] + ((flSlenderMins[i2] + flSlenderMaxs[i2]) / 2.0); + } + + if (IsClientInGhostMode(client)) + { + } + else if (!IsClientInDeathCam(client)) + { + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + + if (!IsPointVisibleToPlayer(client, flSlenderEyePos, true, SlenderUsesBlink(i))) + { + g_bPlayerSeesSlender[client][i] = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, true, SlenderUsesBlink(i)); + } + else + { + g_bPlayerSeesSlender[client][i] = true; + } + + if ((GetGameTime() - g_flPlayerSeesSlenderLastTime[client][i]) > GetProfileFloat(sProfile, "static_on_look_gracetime", 1.0) || + (iOldStaticMode[i] == Static_Increase && g_flPlayerStaticAmount[client] > 0.1)) + { + if ((NPCGetFlags(i) & SFF_STATICONLOOK) && + g_bPlayerSeesSlender[client][i]) + { + if (iCopyMaster != -1) + { + g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; + } + else + { + g_iPlayerStaticMode[client][i] = Static_Increase; + } + } + else if ((NPCGetFlags(i) & SFF_STATICONRADIUS) && + GetVectorDistance(flMyPos, flSlenderPos) <= g_flSlenderStaticRadius[i]) + { + new bool:bNoObstacles = IsPointVisibleToPlayer(client, flSlenderEyePos, false, false); + if (!bNoObstacles) bNoObstacles = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, false); + + if (bNoObstacles) + { + if (iCopyMaster != -1) + { + g_iPlayerStaticMode[client][iCopyMaster] = Static_Increase; + } + else + { + g_iPlayerStaticMode[client][i] = Static_Increase; + } + } + } + } + + // Process death cam sequence conditions + if (SlenderKillsOnNear(i)) + { + if (g_flPlayerStaticAmount[client] >= 1.0 || + GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetInstantKillRadius(i)) + { + new bool:bKillPlayer = true; + if (g_flPlayerStaticAmount[client] < 1.0) + { + bKillPlayer = IsPointVisibleToPlayer(client, flSlenderEyePos, false, SlenderUsesBlink(i)); + } + + if (!bKillPlayer) bKillPlayer = IsPointVisibleToPlayer(client, flSlenderOBBCenterPos, false, SlenderUsesBlink(i)); + + if (bKillPlayer) + { + g_flSlenderLastKill[i] = GetGameTime(); + + if (g_flPlayerStaticAmount[client] >= 1.0) + { + ClientStartDeathCam(client, NPCGetFromUniqueID(g_iPlayerStaticMaster[client]), flSlenderPos); + } + else + { + ClientStartDeathCam(client, i, flSlenderPos); + } + } + } + } + } + } + + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[i]); + if (iMaster == -1) iMaster = i; + + // Boss visiblity. + if (g_bPlayerSeesSlender[client][i] && !bWasSeeingSlender[i]) + { + g_flPlayerSeesSlenderLastTime[client][iMaster] = GetGameTime(); + + if (GetGameTime() >= g_flPlayerScareNextTime[client][iMaster]) + { + if (GetVectorDistance(flMyPos, flSlenderPos) <= NPCGetScareRadius(i)) + { + ClientPerformScare(client, iMaster); + + if (NPCHasAttribute(iMaster, "ignite player on scare")) + { + new Float:flValue = NPCGetAttributeValue(iMaster, "ignite player on scare"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + } + else + { + g_flPlayerScareNextTime[client][iMaster] = GetGameTime() + GetProfileFloat(sProfile, "scare_cooldown"); + } + } + + if (NPCGetType(i) == SF2BossType_Static) + { + if (NPCGetFlags(i) & SFF_FAKE) + { + SlenderMarkAsFake(i); + return; + } + } + + Call_StartForward(fOnClientLooksAtBoss); + Call_PushCell(client); + Call_PushCell(i); + Call_Finish(); + } + else if (!g_bPlayerSeesSlender[client][i] && bWasSeeingSlender[i]) + { + g_flPlayerScareLastTime[client][iMaster] = GetGameTime(); + + Call_StartForward(fOnClientLooksAwayFromBoss); + Call_PushCell(client); + Call_PushCell(i); + Call_Finish(); + } + + if (g_bPlayerSeesSlender[client][i]) + { + if (GetGameTime() >= g_flPlayerSightSoundNextTime[client][iMaster]) + { + ClientPerformSightSound(client, i); + } + } + + if (g_iPlayerStaticMode[client][i] == Static_Increase && + iOldStaticMode[i] != Static_Increase) + { + if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) + { + decl String:sLoopSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); + + if (sLoopSound[0]) + { + EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, GetProfileNum(sProfile, "sound_static_loop_local_level", SNDLEVEL_NORMAL), SND_CHANGEVOL, 1.0); + ClientAddStress(client, 0.03); + } + else + { + LogError("Warning! Boss %s supports static loop local sounds, but was given a blank sound path!", sProfile); + } + } + } + else if (g_iPlayerStaticMode[client][i] != Static_Increase && + iOldStaticMode[i] == Static_Increase) + { + if (NPCGetFlags(i) & SFF_HASSTATICLOOPLOCALSOUND) + { + if (iBoss && iBoss != INVALID_ENT_REFERENCE) + { + decl String:sLoopSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static_loop_local", sLoopSound, sizeof(sLoopSound), 1); + + if (sLoopSound[0]) + { + EmitSoundToClient(client, sLoopSound, iBoss, SNDCHAN_STATIC, _, SND_CHANGEVOL | SND_STOP, 0.0); + } + } + } + } + } + + // Initialize static timers. + new iBossLastStatic = NPCGetFromUniqueID(g_iPlayerStaticMaster[client]); + new iBossNewStatic = -1; + if (iBossLastStatic != -1 && g_iPlayerStaticMode[client][iBossLastStatic] == Static_Increase) + { + iBossNewStatic = iBossLastStatic; + } + + for (new i = 0; i < MAX_BOSSES; i++) + { + new iStaticMode = g_iPlayerStaticMode[client][i]; + + // Determine new static rates. + if (iStaticMode != Static_Increase) continue; + + if (iBossLastStatic == -1 || + g_iPlayerStaticMode[client][iBossLastStatic] != Static_Increase || + NPCGetAnger(i) > NPCGetAnger(iBossLastStatic)) + { + iBossNewStatic = i; + } + } + + if (iBossNewStatic != -1) + { + new iCopyMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossNewStatic]); + if (iCopyMaster != -1) + { + iBossNewStatic = iCopyMaster; + g_iPlayerStaticMaster[client] = NPCGetUniqueID(iCopyMaster); + } + else + { + g_iPlayerStaticMaster[client] = NPCGetUniqueID(iBossNewStatic); + } + } + else + { + g_iPlayerStaticMaster[client] = -1; + } + + if (iBossNewStatic != iBossLastStatic) + { + if (!StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) + { + // Stop last-last static sound entirely. + if (g_strPlayerLastStaticSound[client][0]) + { + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + } + } + + // Move everything down towards the last arrays. + if (g_strPlayerStaticSound[client][0]) + { + strcopy(g_strPlayerLastStaticSound[client], sizeof(g_strPlayerLastStaticSound[]), g_strPlayerStaticSound[client]); + } + + if (iBossNewStatic == -1) + { + // No one is the static master. + g_hPlayerStaticTimer[client] = CreateTimer(g_flPlayerStaticDecreaseRate[client], + Timer_ClientDecreaseStatic, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerStaticTimer[client], true); + } + else + { + NPCGetProfile(iBossNewStatic, sProfile, sizeof(sProfile)); + + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), ""); + + new String:sStaticSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_static", sStaticSound, sizeof(sStaticSound), 1); + + if (sStaticSound[0]) + { + strcopy(g_strPlayerStaticSound[client], sizeof(g_strPlayerStaticSound[]), sStaticSound); + } + + // Cross-fade out the static sounds. + g_flPlayerLastStaticVolume[client] = g_flPlayerStaticAmount[client]; + g_flPlayerLastStaticTime[client] = GetGameTime(); + + g_hPlayerLastStaticTimer[client] = CreateTimer(0.0, + Timer_ClientFadeOutLastStaticSound, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerLastStaticTimer[client], true); + + // Start up our own static timer. + new Float:flStaticIncreaseRate = GetProfileFloat(sProfile, "static_rate") / g_flRoundDifficultyModifier; + new Float:flStaticDecreaseRate = GetProfileFloat(sProfile, "static_rate_decay"); + + g_flPlayerStaticIncreaseRate[client] = flStaticIncreaseRate; + g_flPlayerStaticDecreaseRate[client] = flStaticDecreaseRate; + + g_hPlayerStaticTimer[client] = CreateTimer(flStaticIncreaseRate, + Timer_ClientIncreaseStatic, + GetClientUserId(client), + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + TriggerTimer(g_hPlayerStaticTimer[client], true); + } + } +} + +ClientProcessViewAngles(client) +{ + if ((!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) && + !DidClientEscape(client)) + { + // Process view bobbing, if enabled. + // This code is based on the code in this page: https://developer.valvesoftware.com/wiki/Camera_Bob + // Many thanks to whomever created it in the first place. + + if (IsPlayerAlive(client)) + { + if (g_bPlayerViewbobEnabled) + { + new Float:flPunchVel[3]; + + if (!g_bPlayerViewbobSprintEnabled || !IsClientReallySprinting(client)) + { + if (GetEntityFlags(client) & FL_ONGROUND) + { + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + new Float:flSpeed = GetVectorLength(flVelocity); + + new Float:flPunchIdle[3]; + + if (flSpeed > 0.0) + { + if (flSpeed >= 60.0) + { + flPunchIdle[0] = Sine(GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_X / 400.0; + flPunchIdle[1] = Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Y / 400.0; + flPunchIdle[2] = Sine(1.6 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + + // Calculate roll. + decl Float:flForward[3], Float:flVelocityDirection[3]; + GetClientEyeAngles(client, flForward); + GetVectorAngles(flVelocity, flVelocityDirection); + + new Float:flYawDiff = AngleDiff(flForward[1], flVelocityDirection[1]); + if (FloatAbs(flYawDiff) > 90.0) flYawDiff = AngleDiff(flForward[1] + 180.0, flVelocityDirection[1]) * -1.0; + + new Float:flWalkSpeed = ClientGetDefaultWalkSpeed(client); + new Float:flRollScalar = flSpeed / flWalkSpeed; + if (flRollScalar > 1.0) flRollScalar = 1.0; + + new Float:flRollScale = (flYawDiff / 90.0) * 0.25 * flRollScalar; + flPunchIdle[0] = 0.0; + flPunchIdle[1] = 0.0; + flPunchIdle[2] = flRollScale * -1.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + + g_flPlayerBreathViewbobPhase[client] += SF2_BREATH_VIEWBOB_SPEED; + if(g_flPlayerBreathViewbobPhase[client] > 3.14159265355) { + g_flPlayerBreathViewbobPhase[client] = 0.0; // Sine cycle + g_flPlayerBreathViewbobXMult[client] = GetRandomFloat(-1.0, 1.0); + g_flPlayerBreathViewbobYMult[client] = GetRandomFloat(-1.0, 1.0); + } + new Float:sine = Sine(g_flPlayerBreathViewbobPhase[client]); + flPunchIdle[0] = SF2_BREATH_VIEWBOB_START * sine * g_flPlayerBreathViewbobXMult[client] + SF2_BREATH_VIEWBOB_AMPLITUDE * sine * g_flPlayerBreathViewbobXMult[client] * float(100 - g_iPlayerSprintPoints[client]) / 100.0; + flPunchIdle[1] = SF2_BREATH_VIEWBOB_START / 2.0 * sine * g_flPlayerBreathViewbobYMult[client] + SF2_BREATH_VIEWBOB_AMPLITUDE / 2.0 * sine * g_flPlayerBreathViewbobYMult[client] * float(100 - g_iPlayerSprintPoints[client]) / 100.0; + flPunchIdle[2] = 0.0;//Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) ;//Sine(2.0 * GetGameTime() * SF2_PLAYER_VIEWBOB_TIMER) * flSpeed * SF2_PLAYER_VIEWBOB_SCALE_Z / 400.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + /* + if (flSpeed < 60.0) + { + flPunchIdle[0] = FloatAbs(Cosine(GetGameTime() * 1.25) * 0.047); + flPunchIdle[1] = Sine(GetGameTime() * 1.25) * 0.075; + flPunchIdle[2] = 0.0; + + AddVectors(flPunchVel, flPunchIdle, flPunchVel); + } + */ + } + } + + if (g_bPlayerViewbobHurtEnabled) + { + // Shake screen the more the player is hurt. + new Float:flHealth = float(GetEntProp(client, Prop_Send, "m_iHealth")); + new Float:flMaxHealth = float(SDKCall(g_hSDKGetMaxHealth, client)); + + decl Float:flPunchVelHurt[3]; + flPunchVelHurt[0] = Sine(1.22 * GetGameTime()) * 48.5 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + flPunchVelHurt[1] = Sine(2.12 * GetGameTime()) * 80.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + flPunchVelHurt[2] = Sine(0.5 * GetGameTime()) * 36.0 * ((flMaxHealth - flHealth) / (flMaxHealth * 0.75)) / flMaxHealth; + + AddVectors(flPunchVel, flPunchVelHurt, flPunchVel); + } + + ClientViewPunch(client, flPunchVel); + } + } + } +} + +public Action:Timer_ClientIncreaseStatic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; + + g_flPlayerStaticAmount[client] += 0.05; + if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; + + if (g_strPlayerStaticSound[client][0]) + { + EmitSoundToClient(client, g_strPlayerStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerStaticAmount[client]); + + if (g_flPlayerStaticAmount[client] >= 0.5) ClientAddStress(client, 0.03); + else + { + ClientAddStress(client, 0.02); + } + } + + return Plugin_Continue; +} + +public Action:Timer_ClientDecreaseStatic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerStaticTimer[client]) return Plugin_Stop; + + g_flPlayerStaticAmount[client] -= 0.05; + if (g_flPlayerStaticAmount[client] < 0.0) g_flPlayerStaticAmount[client] = 0.0; + + if (g_strPlayerLastStaticSound[client][0]) + { + new Float:flVolume = g_flPlayerStaticAmount[client]; + if (flVolume > 0.0) + { + EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); + } + } + + if (g_flPlayerStaticAmount[client] <= 0.0) + { + // I've done my job; no point to keep on doing it. + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + g_hPlayerStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_ClientFadeOutLastStaticSound(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerLastStaticTimer[client]) return Plugin_Stop; + + if (StrEqual(g_strPlayerLastStaticSound[client], g_strPlayerStaticSound[client], false)) + { + // Wait, the player's current static sound is the same one we're stopping. Abort! + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + if (g_strPlayerLastStaticSound[client][0]) + { + new Float:flDiff = (GetGameTime() - g_flPlayerLastStaticTime[client]) / 1.0; + if (flDiff > 1.0) flDiff = 1.0; + + new Float:flVolume = g_flPlayerLastStaticVolume[client] - flDiff; + if (flVolume < 0.0) flVolume = 0.0; + + if (flVolume <= 0.0) + { + // I've done my job; no point to keep on doing it. + StopSound(client, SNDCHAN_STATIC, g_strPlayerLastStaticSound[client]); + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + else + { + EmitSoundToClient(client, g_strPlayerLastStaticSound[client], _, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_CHANGEVOL, flVolume); + } + } + else + { + // I've done my job; no point to keep on doing it. + g_hPlayerLastStaticTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +// ========================================================== +// INTERACTIVE GLOW FUNCTIONS +// ========================================================== + +static ClientProcessInteractiveGlow(client) +{ + if (!IsClientInGame(client) || !IsPlayerAlive(client) || (g_bPlayerEliminated[client] && !g_bPlayerProxy[client]) || IsClientInGhostMode(client)) return; + + new iOldLookEntity = EntRefToEntIndex(g_iPlayerInteractiveGlowTargetEntity[client]); + + decl Float:flStartPos[3], Float:flMyEyeAng[3]; + GetClientEyePosition(client, flStartPos); + GetClientEyeAngles(client, flMyEyeAng); + + new Handle:hTrace = TR_TraceRayFilterEx(flStartPos, flMyEyeAng, MASK_VISIBLE, RayType_Infinite, TraceRayDontHitPlayers, -1); + new iEnt = TR_GetEntityIndex(hTrace); + CloseHandle(hTrace); + + if (IsValidEntity(iEnt)) + { + g_iPlayerInteractiveGlowTargetEntity[client] = EntRefToEntIndex(iEnt); + } + else + { + g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; + } + + if (iEnt != iOldLookEntity) + { + ClientRemoveInteractiveGlow(client); + + if (IsEntityClassname(iEnt, "prop_dynamic", false)) + { + decl String:sTargetName[64]; + GetEntPropString(iEnt, Prop_Data, "m_iName", sTargetName, sizeof(sTargetName)); + + if (StrContains(sTargetName, "sf2_page", false) == 0 || StrContains(sTargetName, "sf2_interact", false) == 0) + { + ClientCreateInteractiveGlow(client, iEnt); + } + } + } +} + +ClientResetInteractiveGlow(client) +{ + ClientRemoveInteractiveGlow(client); + g_iPlayerInteractiveGlowTargetEntity[client] = INVALID_ENT_REFERENCE; +} + +/** + * Removes the player's current interactive glow entity. + */ +ClientRemoveInteractiveGlow(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientRemoveInteractiveGlow(%d)", client); +#endif + + new ent = EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerInteractiveGlowEntity[client] = INVALID_ENT_REFERENCE; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientRemoveInteractiveGlow(%d)", client); +#endif +} + +/** + * Creates an interactive glow for an entity to show to a player. + */ +bool:ClientCreateInteractiveGlow(client, iEnt, const String:sAttachment[]="") +{ + ClientRemoveInteractiveGlow(client); + + if (!IsClientInGame(client)) return false; + + if (!iEnt || !IsValidEdict(iEnt)) return false; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientCreateInteractiveGlow(%d)", client); +#endif + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetEntPropString(iEnt, Prop_Data, "m_ModelName", sBuffer, sizeof(sBuffer)); + + if (strlen(sBuffer) == 0) + { + return false; + } + + new ent = CreateEntityByName("tf_taunt_prop"); + if (ent != -1) + { + g_iPlayerInteractiveGlowEntity[client] = EntIndexToEntRef(ent); + + new Float:flModelScale = GetEntPropFloat(iEnt, Prop_Send, "m_flModelScale"); + + SetEntityModel(ent, sBuffer); + DispatchSpawn(ent); + ActivateEntity(ent); + SetEntityRenderMode(ent, RENDER_TRANSCOLOR); + SetEntityRenderColor(ent, 0, 0, 0, 0); + SetEntProp(ent, Prop_Send, "m_bGlowEnabled", 1); + SetEntPropFloat(ent, Prop_Send, "m_flModelScale", flModelScale); + + new iFlags = GetEntProp(ent, Prop_Send, "m_fEffects"); + SetEntProp(ent, Prop_Send, "m_fEffects", iFlags | (1 << 0)); + + SetVariantString("!activator"); + AcceptEntityInput(ent, "SetParent", iEnt); + + if (sAttachment[0]) + { + SetVariantString(sAttachment); + AcceptEntityInput(ent, "SetParentAttachment"); + } + + SDKHook(ent, SDKHook_SetTransmit, Hook_InterativeGlowSetTransmit); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> true", client); +#endif + + return true; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientCreateInteractiveGlow(%d) -> false", client); +#endif + + return false; +} + +public Action:Hook_InterativeGlowSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerInteractiveGlowEntity[other]) != ent) return Plugin_Handled; + + return Plugin_Continue; +} + +// ========================================================== +// BREATHING FUNCTIONS +// ========================================================== + +ClientResetBreathing(client) +{ + g_bPlayerBreath[client] = false; + g_hPlayerBreathTimer[client] = INVALID_HANDLE; +} + +Float:ClientCalculateBreathingCooldown(client) +{ + new Float:flAverage = 0.0; + new iAverageNum = 0; + + // Sprinting only, for now. + flAverage += (SF2_PLAYER_BREATH_COOLDOWN_MAX * 6.7765 * Pow((float(g_iPlayerSprintPoints[client]) / 100.0), 1.65)); + iAverageNum++; + + flAverage /= float(iAverageNum) + + if (flAverage < SF2_PLAYER_BREATH_COOLDOWN_MIN) flAverage = SF2_PLAYER_BREATH_COOLDOWN_MIN; + + return flAverage; +} + +ClientStartBreathing(client) +{ + g_bPlayerBreath[client] = true; + g_hPlayerBreathTimer[client] = CreateTimer(ClientCalculateBreathingCooldown(client), Timer_ClientBreath, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStopBreathing(client) +{ + g_bPlayerBreath[client] = false; + g_hPlayerBreathTimer[client] = INVALID_HANDLE; +} + +bool:ClientCanBreath(client) +{ + return bool:(ClientCalculateBreathingCooldown(client) < SF2_PLAYER_BREATH_COOLDOWN_MAX); +} + +public Action:Timer_ClientBreath(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerBreathTimer[client]) return; + + if (!g_bPlayerBreath[client]) return; + + if (ClientCanBreath(client)) + { + EmitSoundToAll(g_strPlayerBreathSounds[GetRandomInt(0, sizeof(g_strPlayerBreathSounds) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + + ClientStartBreathing(client); + return; + } + + ClientStopBreathing(client); +} + +// ========================================================== +// SPRINTING FUNCTIONS +// ========================================================== + +bool:IsClientSprinting(client) +{ + return g_bPlayerSprint[client]; +} + +ClientGetSprintPoints(client) +{ + return g_iPlayerSprintPoints[client]; +} + +ClientResetSprint(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSprint(%d)", client); +#endif + + g_bPlayerSprint[client] = false; + g_iPlayerSprintPoints[client] = 100; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + + if (IsValidClient(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSprint(%d)", client); +#endif +} + +ClientStartSprint(client) +{ + if (IsClientSprinting(client)) return; + + g_bPlayerSprint[client] = true; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + ClientSprintTimer(client); + TriggerTimer(g_hPlayerSprintTimer[client], true); + + SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); +} + +static ClientSprintTimer(client, bool:bRecharge=false) +{ + new Float:flRate = 0.28; + if (bRecharge) flRate = 0.8; + + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + + if (bRecharge) + { + if (!(GetEntityFlags(client) & FL_ONGROUND)) flRate *= 0.75; + else if (GetVectorLength(flVelocity) == 0.0) + { + if (GetEntProp(client, Prop_Send, "m_bDucked")) flRate *= 0.66; + else flRate *= 0.75; + } + } + else + { + if (TF2_GetPlayerClass(client) == TFClass_Scout) flRate *= 1.15; + } + + if (bRecharge) g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientRechargeSprint, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + else g_hPlayerSprintTimer[client] = CreateTimer(flRate, Timer_ClientSprinting, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStopSprint(client) +{ + if (!IsClientSprinting(client)) return; + + g_bPlayerSprint[client] = false; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + ClientSprintTimer(client, true); + + SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); +} + +bool:IsClientReallySprinting(client) +{ + if (!IsClientSprinting(client)) return false; + if (!(GetEntityFlags(client) & FL_ONGROUND)) return false; + + decl Float:flVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", flVelocity); + if (GetVectorLength(flVelocity) < 30.0) return false; + + return true; +} + +public Action:Timer_ClientSprinting(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSprintTimer[client]) return; + + if (!IsClientSprinting(client)) return; + + if (g_iPlayerSprintPoints[client] <= 0) + { + ClientStopSprint(client); + g_iPlayerSprintPoints[client] = 0; + return; + } + + if (IsClientReallySprinting(client)) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); + if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) + { + g_iPlayerSprintPoints[client]--; + } + } + + ClientSprintTimer(client); +} + +public Hook_ClientSprintingPreThink(client) +{ + if (!IsClientReallySprinting(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + SDKHook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + return; + }} + +public Hook_ClientRechargeSprintPreThink(client) +{ + if (IsClientReallySprinting(client)) + { + SDKUnhook(client, SDKHook_PreThink, Hook_ClientRechargeSprintPreThink); + SDKHook(client, SDKHook_PreThink, Hook_ClientSprintingPreThink); + return; + } +} + +public Action:Timer_ClientRechargeSprint(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerSprintTimer[client]) return; + + if (IsClientSprinting(client)) + { + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + return; + } + + if (g_iPlayerSprintPoints[client] >= 100) + { + g_iPlayerSprintPoints[client] = 100; + g_hPlayerSprintTimer[client] = INVALID_HANDLE; + return; + } + + g_iPlayerSprintPoints[client]++; + ClientSprintTimer(client, true); +} + +// ========================================================== +// PROXY / GHOST AND GLOW FUNCTIONS +// ========================================================== + +ClientResetProxy(client, bool:bResetFull=true) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetProxy(%d)", client); +#endif + + new iOldMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + new String:sOldProfileName[SF2_MAX_PROFILE_NAME_LENGTH]; + if (iOldMaster >= 0) + { + NPCGetProfile(iOldMaster, sOldProfileName, sizeof(sOldProfileName)); + } + + new bool:bOldProxy = g_bPlayerProxy[client]; + if (bResetFull) + { + g_bPlayerProxy[client] = false; + g_iPlayerProxyMaster[client] = -1; + } + + g_iPlayerProxyControl[client] = 0; + g_hPlayerProxyControlTimer[client] = INVALID_HANDLE; + g_flPlayerProxyControlRate[client] = 0.0; + g_flPlayerProxyVoiceTimer[client] = INVALID_HANDLE; + + if (IsClientInGame(client)) + { + if (bOldProxy) + { + ClientStartProxyAvailableTimer(client); + + if (bResetFull) + { + SetVariantString(""); + AcceptEntityInput(client, "SetCustomModel"); + } + + if (sOldProfileName[0]) + { + ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_spawn", GetProfileNum(sOldProfileName, "sound_proxy_spawn_channel", SNDCHAN_AUTO)); + ClientStopAllSlenderSounds(client, sOldProfileName, "sound_proxy_hurt", GetProfileNum(sOldProfileName, "sound_proxy_hurt_channel", SNDCHAN_AUTO)); + } + } + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetProxy(%d)", client); +#endif +} + +ClientStartProxyAvailableTimer(client) +{ + g_bPlayerProxyAvailable[client] = false; + g_hPlayerProxyAvailableTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerProxyWaitTime), Timer_ClientProxyAvailable, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); +} + +ClientStartProxyForce(client, iSlenderID, const Float:flPos[3]) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); +#endif + + g_iPlayerProxyAskMaster[client] = iSlenderID; + for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; + + g_iPlayerProxyAvailableCount[client] = 0; + g_bPlayerProxyAvailableInForce[client] = true; + g_hPlayerProxyAvailableTimer[client] = CreateTimer(1.0, Timer_ClientForceProxy, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerProxyAvailableTimer[client], true); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientStartProxyForce(%d, %d, flPos)", client, iSlenderID); +#endif +} + +ClientStopProxyForce(client) +{ + g_iPlayerProxyAvailableCount[client] = 0; + g_bPlayerProxyAvailableInForce[client] = false; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; +} + +public Action:Timer_ClientForceProxy(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerProxyAvailableTimer[client]) return Plugin_Stop; + + if (!IsRoundEnding()) + { + new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[client]); + if (iBossIndex != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumProxies = 0; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; + + iNumProxies++; + } + + if (iNumProxies < iMaxProxies) + { + if (g_iPlayerProxyAvailableCount[client] > 0) + { + g_iPlayerProxyAvailableCount[client]--; + + SetHudTextParams(-1.0, 0.25, + 1.0, + 255, 255, 255, 255, + _, + _, + 0.25, 1.25); + + ShowSyncHudText(client, g_hHudSync, "%T", "SF2 Proxy Force Message", client, g_iPlayerProxyAvailableCount[client]); + + return Plugin_Continue; + } + else + { + ClientEnableProxy(client, iBossIndex); + TeleportEntity(client, g_iPlayerProxyAskPosition[client], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + } + } + else + { + PrintToChat(client, "%T", "SF2 Too Many Proxies", client); + } + } + } + + ClientStopProxyForce(client); + return Plugin_Stop; +} + +DisplayProxyAskMenu(client, iAskMaster, const Float:flPos[3]) +{ + decl String:sBuffer[512]; + new Handle:hMenu = CreateMenu(Menu_ProxyAsk); + SetMenuTitle(hMenu, "%T\n \n%T\n \n", "SF2 Proxy Ask Menu Title", client, "SF2 Proxy Ask Menu Description", client); + + Format(sBuffer, sizeof(sBuffer), "%T", "Yes", client); + AddMenuItem(hMenu, "1", sBuffer); + Format(sBuffer, sizeof(sBuffer), "%T", "No", client); + AddMenuItem(hMenu, "0", sBuffer); + + g_iPlayerProxyAskMaster[client] = iAskMaster; + for (new i = 0; i < 3; i++) g_iPlayerProxyAskPosition[client][i] = flPos[i]; + DisplayMenu(hMenu, client, 15); +} + +public Menu_ProxyAsk(Handle:menu, MenuAction:action, param1, param2) +{ + switch (action) + { + case MenuAction_End: CloseHandle(menu); + case MenuAction_Select: + { + if (!IsRoundEnding()) + { + new iBossIndex = NPCGetFromUniqueID(g_iPlayerProxyAskMaster[param1]); + if (iBossIndex != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new iMaxProxies = GetProfileNum(sProfile, "proxies_max"); + new iNumProxies; + + for (new iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !g_bPlayerEliminated[iClient]) continue; + if (!g_bPlayerProxy[iClient]) continue; + if (NPCGetFromUniqueID(g_iPlayerProxyMaster[iClient]) != iBossIndex) continue; + + iNumProxies++; + } + + if (iNumProxies < iMaxProxies) + { + if (param2 == 0) + { + ClientEnableProxy(param1, iBossIndex); + TeleportEntity(param1, g_iPlayerProxyAskPosition[param1], NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + } + else + { + ClientStartProxyAvailableTimer(param1); + } + } + else + { + PrintToChat(param1, "%T", "SF2 Too Many Proxies", param1); + } + } + } + } + } +} + +public Action:Timer_ClientProxyAvailable(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerProxyAvailableTimer[client]) return; + + g_bPlayerProxyAvailable[client] = true; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; +} + +ClientEnableProxy(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) return; + if (!(NPCGetFlags(iBossIndex) & SFF_PROXIES)) return; + if (g_bPlayerProxy[client]) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + PvP_SetPlayerPvPState(client, false, false, false); + + ClientSetGhostModeState(client, false); + + ClientStopProxyForce(client); + + ChangeClientTeamNoSuicide(client, _:TFTeam_Blue); + if (!IsPlayerAlive(client)) TF2_RespawnPlayer(client); + // Speed recalculation. Props to the creators of FF2/VSH for this snippet. + TF2_AddCondition(client, TFCond_SpeedBuffAlly, 0.001); + + g_bPlayerProxy[client] = true; + g_iPlayerProxyMaster[client] = NPCGetUniqueID(iBossIndex); + g_iPlayerProxyControl[client] = 100; + g_flPlayerProxyControlRate[client] = GetProfileFloat(sProfile, "proxies_controldrainrate"); + g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + g_bPlayerProxyAvailable[client] = false; + g_hPlayerProxyAvailableTimer[client] = INVALID_HANDLE; + + decl String:sAllowedClasses[512]; + GetProfileString(sProfile, "proxies_classes", sAllowedClasses, sizeof(sAllowedClasses)); + + decl String:sClassName[64]; + TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); + if (sAllowedClasses[0] && sClassName[0] && StrContains(sAllowedClasses, sClassName, false) == -1) + { + // Pick the first class that's allowed. + new String:sAllowedClassesList[32][32]; + new iClassCount = ExplodeString(sAllowedClasses, " ", sAllowedClassesList, 32, 32); + if (iClassCount) + { + TF2_SetPlayerClass(client, TF2_GetClass(sAllowedClassesList[0]), _, false); + + new iMaxHealth = GetEntProp(client, Prop_Send, "m_iHealth"); + TF2_RegeneratePlayer(client); + SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); + SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); + } + } + + UTIL_ScreenFade(client, 200, 1, FFADE_IN, 255, 255, 255, 100); + PrecacheSound("weapons/teleporter_send.wav"); + EmitSoundToClient(client, "weapons/teleporter_send.wav", _, SNDCHAN_STATIC); + + ClientActivateUltravision(client); + + CreateTimer(0.33, Timer_ApplyCustomModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + Call_StartForward(fOnClientSpawnedAsProxy); + Call_PushCell(client); + Call_Finish(); +} + +public Action:Timer_ClientProxyControl(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerProxyControlTimer[client]) return; + + g_iPlayerProxyControl[client]--; + if (g_iPlayerProxyControl[client] <= 0) + { + // ForcePlayerSuicide isn't really dependable, since the player doesn't suicide until several seconds after spawning has passed. + SDKHooks_TakeDamage(client, client, client, 9001.0, DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + return; + } + + g_hPlayerProxyControlTimer[client] = CreateTimer(g_flPlayerProxyControlRate[client], Timer_ClientProxyControl, userid, TIMER_FLAG_NO_MAPCHANGE); +} + +bool:DoesClientHaveConstantGlow(client) +{ + return g_bPlayerConstantGlowEnabled[client]; +} + +ClientDisableConstantGlow(client) +{ + if (!DoesClientHaveConstantGlow(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientDisableConstantGlow(%d)", client); +#endif + + g_bPlayerConstantGlowEnabled[client] = false; + + new iGlow = EntRefToEntIndex(g_iPlayerConstantGlowEntity[client]); + if (iGlow && iGlow != INVALID_ENT_REFERENCE) AcceptEntityInput(iGlow, "Kill"); + + g_iPlayerConstantGlowEntity[client] = INVALID_ENT_REFERENCE; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientDisableConstantGlow(%d)", client); +#endif +} + +bool:ClientEnableConstantGlow(client, const String:sAttachment[]="") +{ + if (DoesClientHaveConstantGlow(client)) return true; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientEnableConstantGlow(%d)", client); +#endif + + decl String:sModel[PLATFORM_MAX_PATH]; + GetClientModel(client, sModel, sizeof(sModel)); + + if (strlen(sModel) == 0) + { + // For some reason the model couldn't be found, so no. + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false (no model specified)", client); +#endif + + return false; + } + + new iGlow = CreateEntityByName("tf_taunt_prop"); + if (iGlow != -1) + { +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> created"); +#endif + + g_bPlayerConstantGlowEnabled[client] = true; + g_iPlayerConstantGlowEntity[client] = EntIndexToEntRef(iGlow); + + new Float:flModelScale = GetEntPropFloat(client, Prop_Send, "m_flModelScale"); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) + { + DebugMessage("tf_taunt_prop -> get model and model scale (%s, %f, player class: %d)", sModel, flModelScale, TF2_GetPlayerClass(client)); + } +#endif + + SetEntityModel(iGlow, sModel); + DispatchSpawn(iGlow); + ActivateEntity(iGlow); + SetEntityRenderMode(iGlow, RENDER_TRANSCOLOR); + SetEntityRenderColor(iGlow, 0, 0, 0, 0); + SetEntProp(iGlow, Prop_Send, "m_bGlowEnabled", 1); + SetEntPropFloat(iGlow, Prop_Send, "m_flModelScale", flModelScale); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set model and model scale"); +#endif + + // Set effect flags. + new iFlags = GetEntProp(iGlow, Prop_Send, "m_fEffects"); + SetEntProp(iGlow, Prop_Send, "m_fEffects", iFlags | (1 << 0)); // EF_BONEMERGE + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set bonemerge flags"); +#endif + + SetVariantString("!activator"); + AcceptEntityInput(iGlow, "SetParent", client); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent to client"); +#endif + + if (sAttachment[0]) + { + SetVariantString(sAttachment); + AcceptEntityInput(iGlow, "SetParentAttachment"); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("tf_taunt_prop -> set parent attachment to %s", sAttachment); +#endif + + SDKHook(iGlow, SDKHook_SetTransmit, Hook_ConstantGlowSetTransmit); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> true", client); +#endif + + return true; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientEnableConstantGlow(%d) -> false", client); +#endif + + return false; +} + +ClientResetJumpScare(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetJumpScare(%d)", client); +#endif + + g_iPlayerJumpScareBoss[client] = -1; + g_flPlayerJumpScareLifeTime[client] = -1.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetJumpScare(%d)", client); +#endif +} + +ClientDoJumpScare(client, iBossIndex, Float:flLifeTime) +{ + g_iPlayerJumpScareBoss[client] = NPCGetUniqueID(iBossIndex); + g_flPlayerJumpScareLifeTime[client] = GetGameTime() + flLifeTime; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_jumpscare", sBuffer, sizeof(sBuffer), 1); + + if (strlen(sBuffer) > 0) + { + EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN); + } +} + + /** + * Handles sprinting upon player input. + */ +ClientHandleSprint(client, bool:bSprint) +{ + if (!IsPlayerAlive(client) || + g_bPlayerEliminated[client] || + DidClientEscape(client) || + g_bPlayerProxy[client] || + IsClientInGhostMode(client)) return; + + if (bSprint) + { + if (g_iPlayerSprintPoints[client] > 0) + { + ClientStartSprint(client); + } + else + { + EmitSoundToClient(client, FLASHLIGHT_NOSOUND, _, SNDCHAN_ITEM, SNDLEVEL_NONE); + } + } + else + { + if (IsClientSprinting(client)) + { + ClientStopSprint(client); + } + } +} + +ClientOnButtonPress(client, button) +{ + switch (button) + { + case IN_ATTACK2: + { + if (IsPlayerAlive(client)) + { + if (!IsRoundInWarmup() && + !IsRoundInIntro() && + !IsRoundEnding() && + !DidClientEscape(client)) + { + if (GetGameTime() >= ClientGetFlashlightNextInputTime(client)) + { + ClientHandleFlashlight(client); + } + } + } + } + case IN_RELOAD: + { + ClientHandleSprint(client, true); + if(IsClientInGhostMode(client) && g_iGhostNextHelpPhrase[client] < GetTime()) { + EmitSoundToAll(g_strGhostHelpPhrases[GetRandomInt(0, sizeof(g_strGhostHelpPhrases) - 1)], client, SNDCHAN_AUTO, SNDLEVEL_SCREAMING); + g_iGhostNextHelpPhrase[client] = GetTime() + g_iGhostHelpPhraseInterval; + } + } + case IN_ATTACK3: + { + if (IsPlayerAlive(client)) + { + if (!g_bPlayerEliminated[client]) + { + if (!IsRoundEnding() && + !IsRoundInWarmup() && + !IsRoundInIntro() && + !DidClientEscape(client)) + { + ClientBlink(client); + } + } + } + } + case IN_JUMP: + { + if (IsPlayerAlive(client) && !(GetEntityFlags(client) & FL_FROZEN)) + { + if (!bool:GetEntProp(client, Prop_Send, "m_bDucked") && + (GetEntityFlags(client) & FL_ONGROUND) && + GetEntProp(client, Prop_Send, "m_nWaterLevel") < 2) + { + ClientOnJump(client); + } + } + } + } +} + +ClientOnButtonRelease(client, button) +{ + switch (button) + { + case IN_RELOAD: + { + ClientHandleSprint(client, false); + } + } +} + +ClientOnJump(client) +{ + if (!g_bPlayerEliminated[client]) + { + if (!IsRoundEnding() && !IsRoundInWarmup() && !DidClientEscape(client)) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteSprintOverride); + if ((!g_bRoundInfiniteSprint && iOverride != 1) || iOverride == 0) + { + g_iPlayerSprintPoints[client] -= 7; + if (g_iPlayerSprintPoints[client] < 0) g_iPlayerSprintPoints[client] = 0; + } + + if (!IsClientSprinting(client)) + { + if (g_hPlayerSprintTimer[client] == INVALID_HANDLE) + { + // If the player hasn't sprinted recently, force us to regenerate the stamina. + ClientSprintTimer(client, true); + } + } + } + } +} + +// ========================================================== +// DEATH CAM FUNCTIONS +// ========================================================== + +bool:IsClientInDeathCam(client) +{ + return g_bPlayerDeathCam[client]; +} + +public Action:Hook_DeathCamSetTransmit(slender, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + if (EntRefToEntIndex(g_iPlayerDeathCamEnt2[other]) != slender) return Plugin_Handled; + return Plugin_Continue; +} + +ClientResetDeathCam(client) +{ + if (!IsClientInDeathCam(client)) return; // no really need to reset if it wasn't set. + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetDeathCam(%d)", client); +#endif + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + + g_iPlayerDeathCamBoss[client] = -1; + g_bPlayerDeathCam[client] = false; + g_bPlayerDeathCamShowOverlay[client] = false; + g_hPlayerDeathCamTimer[client] = INVALID_HANDLE; + + new ent = EntRefToEntIndex(g_iPlayerDeathCamEnt[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Disable"); + AcceptEntityInput(ent, "Kill"); + } + + ent = EntRefToEntIndex(g_iPlayerDeathCamEnt2[client]); + if (ent && ent != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(ent, "Kill"); + } + + g_iPlayerDeathCamEnt[client] = INVALID_ENT_REFERENCE; + g_iPlayerDeathCamEnt2[client] = INVALID_ENT_REFERENCE; + + if (IsClientInGame(client)) + { + SetClientViewEntity(client, client); + } + + Call_StartForward(fOnClientEndDeathCam); + Call_PushCell(client); + Call_PushCell(iDeathCamBoss); + Call_Finish(); + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetDeathCam(%d)", client); +#endif +} + +ClientStartDeathCam(client, iBossIndex, const Float:vecLookPos[3]) +{ + if (IsClientInDeathCam(client)) return; + if (!NPCIsValid(iBossIndex)) return; + + decl String:buffer[PLATFORM_MAX_PATH]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + if (GetProfileNum(sProfile, "death_cam_play_scare_sound")) + { + GetRandomStringFromProfile(sProfile, "sound_scare_player", buffer, sizeof(buffer)); + if (buffer[0]) EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + + GetRandomStringFromProfile(sProfile, "sound_player_deathcam", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + else + { + // Legacy support for "sound_player_death" + GetRandomStringFromProfile(sProfile, "sound_player_death", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToClient(client, buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + + GetRandomStringFromProfile(sProfile, "sound_player_deathcam_all", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + else + { + // Legacy support for "sound_player_death_all" + GetRandomStringFromProfile(sProfile, "sound_player_death_all", buffer, sizeof(buffer)); + if (strlen(buffer) > 0) + { + EmitSoundToAll(buffer, _, MUSIC_CHAN, SNDLEVEL_NONE); + } + } + + // Call our forward. + Call_StartForward(fOnClientCaughtByBoss); + Call_PushCell(client); + Call_PushCell(iBossIndex); + Call_Finish(); + + if (!NPCHasDeathCamEnabled(iBossIndex)) + { + SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol changes our lifestate. + + // TODO: Add more attributes! + if (NPCHasAttribute(iBossIndex, "ignite player on death")) + { + new Float:flValue = NPCGetAttributeValue(iBossIndex, "ignite player on death"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + + SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + return; + } + + g_iPlayerDeathCamBoss[client] = NPCGetUniqueID(iBossIndex); + g_bPlayerDeathCam[client] = true; + g_bPlayerDeathCamShowOverlay[client] = false; + + decl Float:eyePos[3], Float:eyeAng[3], Float:vecAng[3]; + GetClientEyePosition(client, eyePos); + GetClientEyeAngles(client, eyeAng); + SubtractVectors(eyePos, vecLookPos, vecAng); + GetVectorAngles(vecAng, vecAng); + vecAng[0] = 0.0; + vecAng[2] = 0.0; + + // Create fake model. + new slender = SpawnSlenderModel(iBossIndex, vecLookPos); + TeleportEntity(slender, vecLookPos, vecAng, NULL_VECTOR); + g_iPlayerDeathCamEnt2[client] = EntIndexToEntRef(slender); + SDKHook(slender, SDKHook_SetTransmit, Hook_DeathCamSetTransmit); + + // Create camera look point. + decl String:sName[64]; + Format(sName, sizeof(sName), "sf2_boss_%d", EntIndexToEntRef(slender)); + + decl Float:flOffsetPos[3]; + new target = CreateEntityByName("info_target"); + GetProfileVector(sProfile, "death_cam_pos", flOffsetPos); + AddVectors(vecLookPos, flOffsetPos, flOffsetPos); + TeleportEntity(target, flOffsetPos, NULL_VECTOR, NULL_VECTOR); + DispatchKeyValue(target, "targetname", sName); + SetVariantString("!activator"); + AcceptEntityInput(target, "SetParent", slender); + + // Create the camera itself. + new camera = CreateEntityByName("point_viewcontrol"); + TeleportEntity(camera, eyePos, eyeAng, NULL_VECTOR); + DispatchKeyValue(camera, "spawnflags", "12"); + DispatchKeyValue(camera, "target", sName); + DispatchSpawn(camera); + AcceptEntityInput(camera, "Enable", client); + g_iPlayerDeathCamEnt[client] = EntIndexToEntRef(camera); + + if (GetProfileNum(sProfile, "death_cam_overlay") && GetProfileFloat(sProfile, "death_cam_time_overlay_start") >= 0.0) + { + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_overlay_start"), Timer_ClientResetDeathCam1, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + else + { + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + } + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, Float:{ 0.0, 0.0, 0.0 }); + + Call_StartForward(fOnClientStartDeathCam); + Call_PushCell(client); + Call_PushCell(iBossIndex); + Call_Finish(); +} + +public Action:Timer_ClientResetDeathCam1(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerDeathCamTimer[client]) return; + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); + + g_bPlayerDeathCamShowOverlay[client] = true; + g_hPlayerDeathCamTimer[client] = CreateTimer(GetProfileFloat(sProfile, "death_cam_time_death"), Timer_ClientResetDeathCamEnd, userid, TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ClientResetDeathCamEnd(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerDeathCamTimer[client]) return; + + SetEntProp(client, Prop_Data, "m_takedamage", 2); // We do this because the point_viewcontrol entity changes our damage state. + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + if (iDeathCamBoss != -1) + { + if (NPCHasAttribute(iDeathCamBoss, "ignite player on death")) + { + new Float:flValue = NPCGetAttributeValue(iDeathCamBoss, "ignite player on death"); + if (flValue > 0.0) TF2_IgnitePlayer(client, client); + } + } + + SDKHooks_TakeDamage(client, 0, 0, 9001.0, 0x80 | DMG_PREVENT_PHYSICS_FORCE, _, Float:{ 0.0, 0.0, 0.0 }); + ClientResetDeathCam(client); +} + +// ========================================================== +// GHOST MODE FUNCTIONS +// ========================================================== + +static bool:g_bPlayerGhostMode[MAXPLAYERS + 1] = { false, ... }; +static g_iPlayerGhostModeTarget[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... }; +static Handle:g_hPlayerGhostModeConnectionCheckTimer[MAXPLAYERS + 1] = { INVALID_HANDLE, ... }; +static Float:g_flPlayerGhostModeConnectionTimeOutTime[MAXPLAYERS + 1] = { -1.0, ... }; +static Float:g_flPlayerGhostModeConnectionBootTime[MAXPLAYERS + 1] = { -1.0, ... }; + +/** + * Enables/Disables ghost mode on the player. + */ +ClientSetGhostModeState(client, bool:bState) +{ + if (bState == g_bPlayerGhostMode[client]) return; + + if (bState && !IsClientInGame(client)) return; + + g_bPlayerGhostMode[client] = bState; + g_iPlayerGhostModeTarget[client] = INVALID_ENT_REFERENCE; + + if (bState) + { + ClientHandleGhostMode(client, true); + + if (GetConVarBool(g_cvGhostModeConnectionCheck)) + { + g_hPlayerGhostModeConnectionCheckTimer[client] = CreateTimer(0.0, Timer_GhostModeConnectionCheck, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + } + + PvP_OnClientGhostModeEnable(client); + } + else + { + DestroySpriteOverlay(client); + g_hPlayerGhostModeConnectionCheckTimer[client] = INVALID_HANDLE; + g_flPlayerGhostModeConnectionTimeOutTime[client] = -1.0; + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + + if (IsClientInGame(client)) + { + TF2_RemoveCondition(client, TFCond_HalloweenGhostMode); + SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_PLAYER); + } + } +} + +public Action:Timer_GhostModeConnectionCheck(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerGhostModeConnectionCheckTimer[client]) return Plugin_Stop; + + if (!IsFakeClient(client) && IsClientTimingOut(client)) + { + new Float:bootTime = g_flPlayerGhostModeConnectionBootTime[client]; + if (bootTime < 0.0) + { + bootTime = GetGameTime() + GetConVarFloat(g_cvGhostModeConnectionTolerance); + g_flPlayerGhostModeConnectionBootTime[client] = bootTime; + g_flPlayerGhostModeConnectionTimeOutTime[client] = GetGameTime(); + } + + if (GetGameTime() >= bootTime) + { + ClientSetGhostModeState(client, false); + TF2_RespawnPlayer(client); + + decl String:authString[128]; + GetClientAuthString(client, authString, sizeof(authString)); + + LogSF2Message("Removed %N (%s) from ghost mode due to timing out for %f seconds", client, authString, GetConVarFloat(g_cvGhostModeConnectionTolerance)); + + new Float:timeOutTime = g_flPlayerGhostModeConnectionTimeOutTime[client]; + CPrintToChat(client, "%T", "SF2 Ghost Mode Bad Connection", client, RoundFloat(bootTime - timeOutTime)); + + return Plugin_Stop; + } + } + else + { + // Player regained connection; reset. + g_flPlayerGhostModeConnectionBootTime[client] = -1.0; + } + + return Plugin_Continue; +} + +/** + * Makes sure that the player is a ghost when ghost mode is activated. + */ +ClientHandleGhostMode(client, bool:bForceSpawn=false) +{ + if (!IsClientInGhostMode(client)) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientHandleGhostMode(%d, %d)", client, bForceSpawn); +#endif + + if (!TF2_IsPlayerInCondition(client, TFCond_HalloweenGhostMode) || bForceSpawn) + { + TF2_AddCondition(client, TFCond_HalloweenGhostMode, -1.0); + SetEntProp(client, Prop_Send, "m_CollisionGroup", COLLISION_GROUP_DEBRIS); + + // Set first observer target. + ClientGhostModeNextTarget(client); + ClientActivateUltravision(client); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientHandleGhostMode(%d, %d)", client, bForceSpawn); +#endif +} + +ClientGhostModeNextTarget(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientGhostModeNextTarget(%d)", client); +#endif + + new iLastTarget = EntRefToEntIndex(g_iPlayerGhostModeTarget[client]); + new iNextTarget = -1; + new iFirstTarget = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && (!g_bPlayerEliminated[i] || g_bPlayerProxy[i]) && !IsClientInGhostMode(i) && !DidClientEscape(i) && IsPlayerAlive(i)) + { + if (iFirstTarget == -1) iFirstTarget = i; + if (i > iLastTarget) + { + iNextTarget = i; + break; + } + } + } + + new iTarget = -1; + if (IsValidClient(iNextTarget)) iTarget = iNextTarget; + else iTarget = iFirstTarget; + + if (IsValidClient(iTarget)) + { + g_iPlayerGhostModeTarget[client] = EntIndexToEntRef(iTarget); + + decl Float:flPos[3], Float:flAng[3], Float:flVelocity[3]; + GetClientAbsOrigin(iTarget, flPos); + GetClientEyeAngles(iTarget, flAng); + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsVelocity", flVelocity); + TeleportEntity(client, flPos, flAng, flVelocity); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientGhostModeNextTarget(%d)", client); +#endif +} + +bool:IsClientInGhostMode(client) +{ + return g_bPlayerGhostMode[client]; +} + +// ========================================================== +// SCARE FUNCTIONS +// ========================================================== + +ClientPerformScare(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) + { + LogError("Could not perform scare on client %d: boss does not exist!", client); + return; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + g_flPlayerScareLastTime[client][iBossIndex] = GetGameTime(); + g_flPlayerScareNextTime[client][iBossIndex] = GetGameTime() + NPCGetScareCooldown(iBossIndex); + + // See how much Sanity should be drained from a scare. + new Float:flStaticAmount = GetProfileFloat(sProfile, "scare_static_amount", 0.0); + g_flPlayerStaticAmount[client] += flStaticAmount; + if (g_flPlayerStaticAmount[client] > 1.0) g_flPlayerStaticAmount[client] = 1.0; + + decl String:sScareSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_scare_player", sScareSound, sizeof(sScareSound)); + + if (sScareSound[0]) + { + EmitSoundToClient(client, sScareSound, _, MUSIC_CHAN, SNDLEVEL_NONE); + + if (NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS) + { + new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); + new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); + + g_flPlayerSightSoundNextTime[client][iBossIndex] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); + } + + if (g_flPlayerStress[client] > 0.4) + { + ClientAddStress(client, 0.4); + } + else + { + ClientAddStress(client, 0.66); + } + } + else + { + if (g_flPlayerStress[client] > 0.4) + { + ClientAddStress(client, 0.3); + } + else + { + ClientAddStress(client, 0.45); + } + } +} + +ClientPerformSightSound(client, iBossIndex) +{ + if (NPCGetUniqueID(iBossIndex) == -1) + { + LogError("Could not perform sight sound on client %d: boss does not exist!", client); + return; + } + + if (!(NPCGetFlags(iBossIndex) & SFF_HASSIGHTSOUNDS)) return; + + new iMaster = NPCGetFromUniqueID(g_iSlenderCopyMaster[iBossIndex]); + if (iMaster == -1) iMaster = iBossIndex; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sSightSound[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_sight", sSightSound, sizeof(sSightSound)); + + if (sSightSound[0]) + { + EmitSoundToClient(client, sSightSound, _, MUSIC_CHAN, SNDLEVEL_NONE); + + new Float:flCooldownMin = GetProfileFloat(sProfile, "sound_sight_cooldown_min", 8.0); + new Float:flCooldownMax = GetProfileFloat(sProfile, "sound_sight_cooldown_max", 14.0); + + g_flPlayerSightSoundNextTime[client][iMaster] = GetGameTime() + GetRandomFloat(flCooldownMin, flCooldownMax); + + decl Float:flBossPos[3], Float:flMyPos[3]; + new iBoss = NPCGetEntIndex(iBossIndex); + GetClientAbsOrigin(client, flMyPos); + GetEntPropVector(iBoss, Prop_Data, "m_vecAbsOrigin", flBossPos); + new Float:flDistUnComfortZone = 400.0; + new Float:flBossDist = GetVectorDistance(flMyPos, flBossPos); + + new Float:flStressScalar = 1.0 + (flDistUnComfortZone / flBossDist); + + ClientAddStress(client, 0.1 * flStressScalar); + } + else + { + LogError("Warning! %s supports sight sounds, but was given a blank sound!", sProfile); + } +} + +ClientResetScare(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetScare(%d)", client); +#endif + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_flPlayerScareNextTime[client][i] = -1.0; + g_flPlayerScareLastTime[client][i] = -1.0; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetScare(%d)", client); +#endif +} + +// ========================================================== +// ANTI-CAMPING FUNCTIONS +// ========================================================== + +stock ClientResetCampingStats(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetCampingStats(%d)", client); +#endif + + g_iPlayerCampingStrikes[client] = 0; + g_hPlayerCampingTimer[client] = INVALID_HANDLE; + g_bPlayerCampingFirstTime[client] = true; + g_flPlayerCampingLastPosition[client][0] = 0.0; + g_flPlayerCampingLastPosition[client][1] = 0.0; + g_flPlayerCampingLastPosition[client][2] = 0.0; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetCampingStats(%d)", client); +#endif +} + +ClientStartCampingTimer(client) +{ + g_hPlayerCampingTimer[client] = CreateTimer(5.0, Timer_ClientCheckCamp, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_ClientCheckCamp(Handle:timer, any:userid) +{ + if (IsRoundInWarmup()) return Plugin_Stop; + + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerCampingTimer[client]) return Plugin_Stop; + + if (IsRoundEnding() || !IsPlayerAlive(client) || g_bPlayerEliminated[client] || DidClientEscape(client)) return Plugin_Stop; + + if (!g_bPlayerCampingFirstTime[client]) + { + decl Float:flPos[3], Float:flMaxs[3], Float:flMins[3]; + GetClientAbsOrigin(client, flPos); + GetEntPropVector(client, Prop_Send, "m_vecMins", flMins); + GetEntPropVector(client, Prop_Send, "m_vecMaxs", flMaxs); + + // Only do something if the player is NOT stuck. + new Float:flDistFromLastPosition = GetVectorDistance(g_flPlayerCampingLastPosition[client], flPos); + new Float:flDistFromClosestBoss = 9999999.0; + new iClosestBoss = -1; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + new iSlender = NPCGetEntIndex(i); + if (!iSlender || iSlender == INVALID_ENT_REFERENCE) continue; + + decl Float:flSlenderPos[3]; + SlenderGetAbsOrigin(i, flSlenderPos); + + new Float:flDist = GetVectorDistance(flSlenderPos, flPos); + if (flDist < flDistFromClosestBoss) + { + iClosestBoss = i; + flDistFromClosestBoss = flDist; + } + } + + if (GetConVarBool(g_cvCampingEnabled) && + !g_bRoundGrace && + !IsSpaceOccupiedIgnorePlayers(flPos, flMins, flMaxs, client) && + g_flPlayerStaticAmount[client] <= GetConVarFloat(g_cvCampingNoStrikeSanity) && + (iClosestBoss == -1 || flDistFromClosestBoss >= GetConVarFloat(g_cvCampingNoStrikeBossDistance)) && + flDistFromLastPosition <= GetConVarFloat(g_cvCampingMinDistance)) + { + g_iPlayerCampingStrikes[client]++; + if (g_iPlayerCampingStrikes[client] < GetConVarInt(g_cvCampingMaxStrikes)) + { + if (g_iPlayerCampingStrikes[client] >= GetConVarInt(g_cvCampingStrikesWarn)) + { + CPrintToChat(client, "{red}%T", "SF2 Camping System Warning", client, (GetConVarInt(g_cvCampingMaxStrikes) - g_iPlayerCampingStrikes[client]) * 5); + } + } + else + { + g_iPlayerCampingStrikes[client] = 0; + ClientStartDeathCam(client, 0, flPos); + } + } + else + { + // Forgiveness. + if (g_iPlayerCampingStrikes[client] > 0) g_iPlayerCampingStrikes[client]--; + } + + g_flPlayerCampingLastPosition[client][0] = flPos[0]; + g_flPlayerCampingLastPosition[client][1] = flPos[1]; + g_flPlayerCampingLastPosition[client][2] = flPos[2]; + } + else + { + g_bPlayerCampingFirstTime[client] = false; + } + + return Plugin_Continue; +} + +// ========================================================== +// BLINK FUNCTIONS +// ========================================================== + +bool:IsClientBlinking(client) +{ + return g_bPlayerBlink[client]; +} + +Float:ClientGetBlinkMeter(client) +{ + return g_flPlayerBlinkMeter[client]; +} + +ClientGetBlinkCount(client) +{ + return g_iPlayerBlinkCount[client]; +} + +/** + * Resets all data on blinking. + */ +ClientResetBlink(client) +{ + g_hPlayerBlinkTimer[client] = INVALID_HANDLE; + g_bPlayerBlink[client] = false; + g_flPlayerBlinkMeter[client] = 1.0; + g_iPlayerBlinkCount[client] = 0; +} + +/** + * Sets the player into a blinking state and blinds the player + */ +ClientBlink(client) +{ + if (IsRoundInWarmup() || DidClientEscape(client)) return; + + if (IsClientBlinking(client)) return; + + g_bPlayerBlink[client] = true; + g_iPlayerBlinkCount[client]++; + g_flPlayerBlinkMeter[client] = 0.0; + g_hPlayerBlinkTimer[client] = CreateTimer(GetConVarFloat(g_cvPlayerBlinkHoldTime), Timer_BlinkTimer2, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); + + UTIL_ScreenFade(client, 100, RoundToFloor(GetConVarFloat(g_cvPlayerBlinkHoldTime) * 1000.0), FFADE_IN, 0, 0, 0, 255); + + Call_StartForward(fOnClientBlink); + Call_PushCell(client); + Call_Finish(); +} + +/** + * Unsets the player from the blinking state. + */ +ClientUnblink(client) +{ + if (!IsClientBlinking(client)) return; + + g_bPlayerBlink[client] = false; + g_hPlayerBlinkTimer[client] = INVALID_HANDLE; + g_flPlayerBlinkMeter[client] = 1.0; +} + +ClientStartDrainingBlinkMeter(client) +{ + g_hPlayerBlinkTimer[client] = CreateTimer(ClientGetBlinkRate(client), Timer_BlinkTimer, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +public Action:Timer_BlinkTimer(Handle:timer, any:userid) +{ + if (IsRoundInWarmup()) return Plugin_Stop; + + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerBlinkTimer[client]) return Plugin_Stop; + + if (IsPlayerAlive(client) && !IsClientInDeathCam(client) && !g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !IsRoundEnding()) + { + new iOverride = GetConVarInt(g_cvPlayerInfiniteBlinkOverride); + if ((!g_bRoundInfiniteBlink && iOverride != 1) || iOverride == 0) + { + g_flPlayerBlinkMeter[client] -= 0.05; + } + + if (g_flPlayerBlinkMeter[client] <= 0.0) + { + ClientBlink(client); + return Plugin_Stop; + } + } + + return Plugin_Continue; +} + +public Action:Timer_BlinkTimer2(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (timer != g_hPlayerBlinkTimer[client]) return; + + ClientUnblink(client); + ClientStartDrainingBlinkMeter(client); +} + +Float:ClientGetBlinkRate(client) +{ + new Float:flValue = GetConVarFloat(g_cvPlayerBlinkRate); + if (GetEntProp(client, Prop_Send, "m_nWaterLevel") >= 3) + { + // Being underwater makes you blink faster, obviously. + flValue *= 0.75; + } + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + if (g_bPlayerSeesSlender[client][i]) + { + flValue *= GetProfileFloat(sProfile, "blink_look_rate_multiply", 1.0); + } + + else if (g_iPlayerStaticMode[client][i] == Static_Increase) + { + flValue *= GetProfileFloat(sProfile, "blink_static_rate_multiply", 1.0); + } + } + + if (TF2_GetPlayerClass(client) == TFClass_Sniper) flValue *= 1.4; + + if (IsClientUsingFlashlight(client)) + { + decl Float:startPos[3], Float:endPos[3], Float:flDirection[3]; + new Float:flLength = SF2_FLASHLIGHT_LENGTH; + GetClientEyePosition(client, startPos); + GetClientEyePosition(client, endPos); + GetClientEyeAngles(client, flDirection); + GetAngleVectors(flDirection, flDirection, NULL_VECTOR, NULL_VECTOR); + NormalizeVector(flDirection, flDirection); + ScaleVector(flDirection, flLength); + AddVectors(endPos, flDirection, endPos); + new Handle:hTrace = TR_TraceRayFilterEx(startPos, endPos, MASK_VISIBLE, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); + TR_GetEndPosition(endPos, hTrace); + new bool:bHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bHit) + { + new Float:flPercent = (GetVectorDistance(startPos, endPos) / flLength); + flPercent *= 3.5; + if (flPercent > 1.0) flPercent = 1.0; + flValue *= flPercent; + } + } + + return flValue; +} + +// ========================================================== +// SCREEN OVERLAY FUNCTIONS +// ========================================================== + +ClientAddStress(client, Float:flStressAmount) +{ + g_flPlayerStress[client] += flStressAmount; + if (g_flPlayerStress[client] < 0.0) g_flPlayerStress[client] = 0.0; + if (g_flPlayerStress[client] > 1.0) g_flPlayerStress[client] = 1.0; + + //PrintCenterText(client, "g_flPlayerStress[%d] = %f", client, g_flPlayerStress[client]); + + SlenderOnClientStressUpdate(client); +} + +stock ClientResetOverlay(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetOverlay(%d)", client); +#endif + + g_hPlayerOverlayCheck[client] = INVALID_HANDLE; + + if (IsClientInGame(client)) + { + ClientCommand(client, "r_screenoverlay \"\""); + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetOverlay(%d)", client); +#endif +} + +public DestroySpriteOverlay(client) +{ + if(IsValidClient(client) && client > 0) { + for(new i = 0; i < sizeof(g_iOverlayRef[]); i++) { + Overlay_Render_Clear_Layer(client, i); + } + g_hOverlayUpdateTimer[client] = INVALID_HANDLE; + } +} + +public Action:Timer_PlayerOverlayCheck(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerOverlayCheck[client]) return Plugin_Stop; + + if (IsRoundInWarmup()) return Plugin_Continue; + + new iDeathCamBoss = NPCGetFromUniqueID(g_iPlayerDeathCamBoss[client]); + new iJumpScareBoss = NPCGetFromUniqueID(g_iPlayerJumpScareBoss[client]); + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + decl String:sMaterial[PLATFORM_MAX_PATH]; + + if (IsClientInDeathCam(client) && iDeathCamBoss != -1 && g_bPlayerDeathCamShowOverlay[client]) + { + DestroySpriteOverlay(client); + NPCGetProfile(iDeathCamBoss, sProfile, sizeof(sProfile)); + GetRandomStringFromProfile(sProfile, "overlay_player_death", sMaterial, sizeof(sMaterial), 1); + } + else if (iJumpScareBoss != -1 && GetGameTime() <= g_flPlayerJumpScareLifeTime[client]) + { + DestroySpriteOverlay(client); + NPCGetProfile(iJumpScareBoss, sProfile, sizeof(sProfile)); + GetRandomStringFromProfile(sProfile, "overlay_jumpscare", sMaterial, sizeof(sMaterial), 1); + } + else if (IsRoundInWarmup() || g_bPlayerEliminated[client] || DidClientEscape(client) && !IsClientInGhostMode(client)) + { + DestroySpriteOverlay(client); + return Plugin_Continue; + } + else + { + if (!g_iPlayerPreferences[client][PlayerPreference_FilmGrain]) + strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT_NO_FILMGRAIN); + else + strcopy(sMaterial, sizeof(sMaterial), SF2_OVERLAY_DEFAULT); + } + + ClientCommand(client, "r_screenoverlay %s", sMaterial); + return Plugin_Continue; +} + +public Hide_Weapon(client) { + if(!IsValidClient(client) || client < 1) return; + new v_model = GetEntPropEnt(client, Prop_Send, "m_hViewModel"); + if(v_model < 1) return; + //SetEntProp(v_model, Prop_Send, "m_nModelIndex", -1); + new EntEffects = GetEntProp(v_model, Prop_Send, "m_fEffects"); + EntEffects |= EF_NODRAW; + SetEntProp(v_model, Prop_Send, "m_fEffects", EntEffects); +} + +public Show_Weapon(client) { + if(!IsValidClient(client) || client < 1) return; + new v_model = GetEntPropEnt(client, Prop_Send, "m_hViewModel"); + if(v_model < 1) return; + //SetEntProp(v_model, Prop_Send, "m_nModelIndex", -1); + new EntEffects = GetEntProp(v_model, Prop_Send, "m_fEffects"); + EntEffects &= ~EF_NODRAW; + SetEntProp(v_model, Prop_Send, "m_fEffects", EntEffects); +} + +// ========================================================== +// MUSIC SYSTEM FUNCTIONS +// ========================================================== + +stock ClientUpdateMusicSystem(client, bool:bInitialize=false) +{ + new iOldPageMusicMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); + new iOldMusicFlags = g_iPlayerMusicFlags[client]; + new iChasingBoss = -1; + new iChasingSeeBoss = -1; + new iAlertBoss = -1; + new i20DollarsBoss = -1; + + if (IsRoundEnding() || !IsClientInGame(client) || IsFakeClient(client) || DidClientEscape(client) || (g_bPlayerEliminated[client] && !IsClientInGhostMode(client) && !g_bPlayerProxy[client])) + { + g_iPlayerMusicFlags[client] = 0; + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + } + else + { + new bool:bPlayMusicOnEscape = true; + decl String:sName[64]; + new ent = -1; + while ((ent = FindEntityByClassname(ent, "info_target")) != -1) + { + GetEntPropString(ent, Prop_Data, "m_iName", sName, sizeof(sName)); + if (StrEqual(sName, "sf2_escape_custommusic", false)) + { + bPlayMusicOnEscape = false; + break; + } + } + + // Page music first. + new iPageRange = 0; + + if (GetArraySize(g_hPageMusicRanges) > 0) // Map has its own defined page music? + { + for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) + { + ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + new iMin = GetArrayCell(g_hPageMusicRanges, i, 1); + new iMax = GetArrayCell(g_hPageMusicRanges, i, 2); + + if (g_iPageCount >= iMin && g_iPageCount <= iMax) + { + g_iPlayerPageMusicMaster[client] = GetArrayCell(g_hPageMusicRanges, i); + break; + } + } + } + else // Nope. Use old system instead. + { + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + + new Float:flPercent = g_iPageMax > 0 ? (float(g_iPageCount) / float(g_iPageMax)) : 0.0; + if (flPercent > 0.0 && flPercent <= 0.25) iPageRange = 1; + else if (flPercent > 0.25 && flPercent <= 0.5) iPageRange = 2; + else if (flPercent > 0.5 && flPercent <= 0.75) iPageRange = 3; + else if (flPercent > 0.75) iPageRange = 4; + + if (iPageRange == 1) ClientAddMusicFlag(client, MUSICF_PAGES1PERCENT); + else if (iPageRange == 2) ClientAddMusicFlag(client, MUSICF_PAGES25PERCENT); + else if (iPageRange == 3) ClientAddMusicFlag(client, MUSICF_PAGES50PERCENT); + else if (iPageRange == 4) ClientAddMusicFlag(client, MUSICF_PAGES75PERCENT); + } + + if (iPageRange != 1) ClientRemoveMusicFlag(client, MUSICF_PAGES1PERCENT); + if (iPageRange != 2) ClientRemoveMusicFlag(client, MUSICF_PAGES25PERCENT); + if (iPageRange != 3) ClientRemoveMusicFlag(client, MUSICF_PAGES50PERCENT); + if (iPageRange != 4) ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); + + if (IsRoundInEscapeObjective() && !bPlayMusicOnEscape) + { + ClientRemoveMusicFlag(client, MUSICF_PAGES75PERCENT); + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; + } + + new iOldChasingBoss = g_iPlayerChaseMusicMaster[client]; + new iOldChasingSeeBoss = g_iPlayerChaseMusicSeeMaster[client]; + new iOldAlertBoss = g_iPlayerAlertMusicMaster[client]; + new iOld20DollarsBoss = g_iPlayer20DollarsMusicMaster[client]; + + new Float:flAnger = -1.0; + new Float:flSeeAnger = -1.0; + new Float:flAlertAnger = -1.0; + new Float:fl20DollarsAnger = -1.0; + + decl Float:flBuffer[3], Float:flBuffer2[3], Float:flBuffer3[3]; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + if (NPCGetUniqueID(i) == -1) continue; + + if (NPCGetEntIndex(i) == INVALID_ENT_REFERENCE) continue; + + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + new iBossType = NPCGetType(i); + + switch (iBossType) + { + case SF2BossType_Chaser: + { + GetClientAbsOrigin(client, flBuffer); + SlenderGetAbsOrigin(i, flBuffer3); + + new iTarget = EntRefToEntIndex(g_iSlenderTarget[i]); + if (iTarget != -1) + { + GetEntPropVector(iTarget, Prop_Data, "m_vecAbsOrigin", flBuffer2); + + if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK || g_iSlenderState[i] == STATE_STUN) && + !(NPCGetFlags(i) & SFF_MARKEDASFAKE) && + (iTarget == client || GetVectorDistance(flBuffer, flBuffer2) <= 850.0 || GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0)) + { + decl String:sPath[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sPath, sizeof(sPath), 1); + if (sPath[0]) + { + if (NPCGetAnger(i) > flAnger) + { + flAnger = NPCGetAnger(i); + iChasingBoss = i; + } + } + + if ((g_iSlenderState[i] == STATE_CHASE || g_iSlenderState[i] == STATE_ATTACK) && + PlayerCanSeeSlender(client, i, false)) + { + if (iOldChasingSeeBoss == -1 || !PlayerCanSeeSlender(client, iOldChasingSeeBoss, false) || (NPCGetAnger(i) > flSeeAnger)) + { + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sPath, sizeof(sPath), 1); + + if (sPath[0]) + { + flSeeAnger = NPCGetAnger(i); + iChasingSeeBoss = i; + } + } + + if (g_b20Dollars) + { + if (iOld20DollarsBoss == -1 || !PlayerCanSeeSlender(client, iOld20DollarsBoss, false) || (NPCGetAnger(i) > fl20DollarsAnger)) + { + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sPath, sizeof(sPath), 1); + + if (sPath[0]) + { + fl20DollarsAnger = NPCGetAnger(i); + i20DollarsBoss = i; + } + } + } + } + } + } + + if (g_iSlenderState[i] == STATE_ALERT) + { + decl String:sPath[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sPath, sizeof(sPath), 1); + if (!sPath[0]) continue; + + if (!(NPCGetFlags(i) & SFF_MARKEDASFAKE)) + { + if (GetVectorDistance(flBuffer, flBuffer3) <= 850.0 || GetVectorDistance(flBuffer, g_flSlenderGoalPos[i]) <= 850.0) + { + if (NPCGetAnger(i) > flAlertAnger) + { + flAlertAnger = NPCGetAnger(i); + iAlertBoss = i; + } + } + } + } + } + } + } + + if (iChasingBoss != iOldChasingBoss) + { + if (iChasingBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_CHASE); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_CHASE); + } + } + + if (iChasingSeeBoss != iOldChasingSeeBoss) + { + if (iChasingSeeBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_CHASEVISIBLE); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_CHASEVISIBLE); + } + } + + if (iAlertBoss != iOldAlertBoss) + { + if (iAlertBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_ALERT); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_ALERT); + } + } + + if (i20DollarsBoss != iOld20DollarsBoss) + { + if (i20DollarsBoss != -1) + { + ClientAddMusicFlag(client, MUSICF_20DOLLARS); + } + else + { + ClientRemoveMusicFlag(client, MUSICF_20DOLLARS); + } + } + } + + if (IsValidClient(client)) + { + new bool:bWasChase = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASE); + new bool:bChase = ClientHasMusicFlag(client, MUSICF_CHASE); + new bool:bWasChaseSee = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_CHASEVISIBLE); + new bool:bChaseSee = ClientHasMusicFlag(client, MUSICF_CHASEVISIBLE); + new bool:bAlert = ClientHasMusicFlag(client, MUSICF_ALERT); + new bool:bWasAlert = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_ALERT); + new bool:b20Dollars = ClientHasMusicFlag(client, MUSICF_20DOLLARS); + new bool:bWas20Dollars = ClientHasMusicFlag2(iOldMusicFlags, MUSICF_20DOLLARS); + + // Custom system. + if (GetArraySize(g_hPageMusicRanges) > 0) + { + decl String:sPath[PLATFORM_MAX_PATH]; + + new iMaster = EntRefToEntIndex(g_iPlayerPageMusicMaster[client]); + if (iMaster != INVALID_ENT_REFERENCE) + { + for (new i = 0, iSize = GetArraySize(g_hPageMusicRanges); i < iSize; i++) + { + new ent = EntRefToEntIndex(GetArrayCell(g_hPageMusicRanges, i)); + if (!ent || ent == INVALID_ENT_REFERENCE) continue; + + GetEntPropString(ent, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + + if (ent == iMaster && + (iOldPageMusicMaster != iMaster || iOldPageMusicMaster == INVALID_ENT_REFERENCE)) + { + if (!sPath[0]) + { + LogError("Could not play music of page range %d-%d: no sound path specified!", GetArrayCell(g_hPageMusicRanges, i, 1), GetArrayCell(g_hPageMusicRanges, i, 2)); + } + else + { + ClientMusicStart(client, sPath, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) + { + GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + StopSound(client, MUSIC_CHAN, sPath); + } + } + } + } + } + else + { + if (iOldPageMusicMaster && iOldPageMusicMaster != INVALID_ENT_REFERENCE) + { + GetEntPropString(iOldPageMusicMaster, Prop_Data, "m_iszSound", sPath, sizeof(sPath)); + if (sPath[0]) + { + StopSound(client, MUSIC_CHAN, sPath); + } + } + } + } + + // Old system. + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES1_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES1PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES1PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES1_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES2_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES25PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES25PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES2_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES3_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES50PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES50PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES3_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + if ((bInitialize || ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && !ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) + { + StopSound(client, MUSIC_CHAN, MUSIC_GOTPAGES4_SOUND); + } + else if ((bInitialize || !ClientHasMusicFlag2(iOldMusicFlags, MUSICF_PAGES75PERCENT)) && ClientHasMusicFlag(client, MUSICF_PAGES75PERCENT)) + { + ClientMusicStart(client, MUSIC_GOTPAGES4_SOUND, _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + + new iMainMusicState = 0; + + if (bAlert != bWasAlert || iAlertBoss != g_iPlayerAlertMusicMaster[client]) + { + if (bAlert && !bChase) + { + ClientAlertMusicStart(client, iAlertBoss); + if (!bWasAlert) iMainMusicState = -1; + } + else + { + ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); + if (!bChase && bWasAlert) iMainMusicState = 1; + } + } + + if (bChase != bWasChase || iChasingBoss != g_iPlayerChaseMusicMaster[client]) + { + if (bChase) + { + ClientMusicChaseStart(client, iChasingBoss); + + if (!bWasChase) + { + iMainMusicState = -1; + + if (bAlert) + { + ClientAlertMusicStop(client, g_iPlayerAlertMusicMaster[client]); + } + } + } + else + { + ClientMusicChaseStop(client, g_iPlayerChaseMusicMaster[client]); + if (bWasChase) + { + if (bAlert) + { + ClientAlertMusicStart(client, iAlertBoss); + } + else + { + iMainMusicState = 1; + } + } + } + } + + if (bChaseSee != bWasChaseSee || iChasingSeeBoss != g_iPlayerChaseMusicSeeMaster[client]) + { + if (bChaseSee) + { + ClientMusicChaseSeeStart(client, iChasingSeeBoss); + } + else + { + ClientMusicChaseSeeStop(client, g_iPlayerChaseMusicSeeMaster[client]); + } + } + + if (b20Dollars != bWas20Dollars || i20DollarsBoss != g_iPlayer20DollarsMusicMaster[client]) + { + if (b20Dollars) + { + Client20DollarsMusicStart(client, i20DollarsBoss); + } + else + { + Client20DollarsMusicStop(client, g_iPlayer20DollarsMusicMaster[client]); + } + } + + if (iMainMusicState == 1) + { + ClientMusicStart(client, g_strPlayerMusic[client], _, MUSIC_PAGE_VOLUME, bChase || bAlert); + } + else if (iMainMusicState == -1) + { + ClientMusicStop(client); + } + + if (bChase || bAlert) + { + new iBossToUse = -1; + if (bChase) + { + iBossToUse = iChasingBoss; + } + else + { + iBossToUse = iAlertBoss; + } + + if (iBossToUse != -1) + { + // We got some alert/chase music going on! The player's excitement will no doubt go up! + // Excitement, though, really depends on how close the boss is in relation to the + // player. + + new Float:flBossDist = NPCGetDistanceFromEntity(iBossToUse, client); + new Float:flScalar = flBossDist / 700.0 + if (flScalar > 1.0) flScalar = 1.0; + new Float:flStressAdd = 0.1 * (1.0 - flScalar); + + ClientAddStress(client, flStressAdd); + } + } + } +} + +stock ClientMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); + strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerMusicFlags[client] = 0; + g_flPlayerMusicVolume[client] = 0.0; + g_flPlayerMusicTargetVolume[client] = 0.0; + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + g_iPlayerPageMusicMaster[client] = INVALID_ENT_REFERENCE; +} + +stock ClientMusicStart(client, const String:sNewMusic[], Float:flVolume=-1.0, Float:flTargetVolume=-1.0, bool:bCopyOnly=false) +{ + if (!IsValidClient(client)) return; + if (!sNewMusic[0]) return; + + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerMusic[client]); + + if (!StrEqual(sOldMusic, sNewMusic, false)) + { + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + + strcopy(g_strPlayerMusic[client], sizeof(g_strPlayerMusic[]), sNewMusic); + if (flVolume >= 0.0) g_flPlayerMusicVolume[client] = flVolume; + if (flTargetVolume >= 0.0) g_flPlayerMusicTargetVolume[client] = flTargetVolume; + + if (!bCopyOnly) + { + g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeInMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerMusicTimer[client], true); + } + else + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + } +} + +stock ClientMusicStop(client) +{ + g_hPlayerMusicTimer[client] = CreateTimer(0.01, Timer_PlayerFadeOutMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerMusicTimer[client], true); +} + +stock Client20DollarsMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayer20DollarsMusic[client]); + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayer20DollarsMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayer20DollarsMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayer20DollarsMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock Client20DollarsMusicStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayer20DollarsMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayer20DollarsMusicMaster[client] = iBossIndex; + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), sBuffer); + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeIn20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientAlertMusicStop(client, iOldMaster); + } +} + +stock Client20DollarsMusicStop(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayer20DollarsMusicMaster[client]) + { + g_iPlayer20DollarsMusicMaster[client] = -1; + strcopy(g_strPlayer20DollarsMusic[client], sizeof(g_strPlayer20DollarsMusic[]), ""); + } + + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOut20DollarsMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayer20DollarsMusicTimer[client][iBossIndex], true); +} + +stock ClientAlertMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerAlertMusic[client]); + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerAlertMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerAlertMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayerAlertMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_alert_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientAlertMusicStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayerAlertMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayerAlertMusicMaster[client] = iBossIndex; + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), sBuffer); + g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientAlertMusicStop(client, iOldMaster); + } +} + +stock ClientAlertMusicStop(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerAlertMusicMaster[client]) + { + g_iPlayerAlertMusicMaster[client] = -1; + strcopy(g_strPlayerAlertMusic[client], sizeof(g_strPlayerAlertMusic[]), ""); + } + + g_hPlayerAlertMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutAlertMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerAlertMusicTimer[client][iBossIndex], true); +} + +stock ClientChaseMusicReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusic[client]); + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); + if (IsValidClient(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerChaseMusicMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerChaseMusicTimer[client][i] = INVALID_HANDLE; + g_flPlayerChaseMusicVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsValidClient(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_chase_music", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientMusicChaseStart(client, iBossIndex) +{ + if (!IsValidClient(client)) return; + + new iOldMaster = g_iPlayerChaseMusicMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); + + if (!sBuffer[0]) return; + + g_iPlayerChaseMusicMaster[client] = iBossIndex; + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), sBuffer); + g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientMusicChaseStop(client, iOldMaster); + } +} + +stock ClientMusicChaseStop(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerChaseMusicMaster[client]) + { + g_iPlayerChaseMusicMaster[client] = -1; + strcopy(g_strPlayerChaseMusic[client], sizeof(g_strPlayerChaseMusic[]), ""); + } + + g_hPlayerChaseMusicTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusic, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicTimer[client][iBossIndex], true); +} + +stock ClientChaseMusicSeeReset(client) +{ + new String:sOldMusic[PLATFORM_MAX_PATH]; + strcopy(sOldMusic, sizeof(sOldMusic), g_strPlayerChaseMusicSee[client]); + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); + if (IsClientInGame(client) && sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + + g_iPlayerChaseMusicSeeMaster[client] = -1; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_hPlayerChaseMusicSeeTimer[client][i] = INVALID_HANDLE; + g_flPlayerChaseMusicSeeVolumes[client][i] = 0.0; + + if (NPCGetUniqueID(i) != -1) + { + if (IsClientInGame(client)) + { + NPCGetProfile(i, sProfile, sizeof(sProfile)); + + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sOldMusic, sizeof(sOldMusic), 1); + if (sOldMusic[0]) StopSound(client, MUSIC_CHAN, sOldMusic); + } + } + } +} + +stock ClientMusicChaseSeeStart(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + + new iOldMaster = g_iPlayerChaseMusicSeeMaster[client]; + if (iOldMaster == iBossIndex) return; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + new String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); + if (!sBuffer[0]) return; + + g_iPlayerChaseMusicSeeMaster[client] = iBossIndex; + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), sBuffer); + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeInChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); + + if (iOldMaster != -1) + { + ClientMusicChaseSeeStop(client, iOldMaster); + } +} + +stock ClientMusicChaseSeeStop(client, iBossIndex) +{ + if (!IsClientInGame(client)) return; + if (iBossIndex == -1) return; + + if (iBossIndex == g_iPlayerChaseMusicSeeMaster[client]) + { + g_iPlayerChaseMusicSeeMaster[client] = -1; + strcopy(g_strPlayerChaseMusicSee[client], sizeof(g_strPlayerChaseMusicSee[]), ""); + } + + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = CreateTimer(0.01, Timer_PlayerFadeOutChaseMusicSee, GetClientUserId(client), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + TriggerTimer(g_hPlayerChaseMusicSeeTimer[client][iBossIndex], true); +} + +public Action:Timer_PlayerFadeInMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; + + g_flPlayerMusicVolume[client] += 0.07; + if (g_flPlayerMusicVolume[client] > g_flPlayerMusicTargetVolume[client]) g_flPlayerMusicVolume[client] = g_flPlayerMusicTargetVolume[client]; + + if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); + + if (g_flPlayerMusicVolume[client] >= g_flPlayerMusicTargetVolume[client]) + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + if (timer != g_hPlayerMusicTimer[client]) return Plugin_Stop; + + g_flPlayerMusicVolume[client] -= 0.07; + if (g_flPlayerMusicVolume[client] < 0.0) g_flPlayerMusicVolume[client] = 0.0; + + if (g_strPlayerMusic[client][0]) EmitSoundToClient(client, g_strPlayerMusic[client], _, MUSIC_CHAN, SNDLEVEL_NONE, SND_CHANGEVOL, g_flPlayerMusicVolume[client]); + + if (g_flPlayerMusicVolume[client] <= 0.0) + { + g_hPlayerMusicTimer[client] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeIn20DollarsMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayer20DollarsMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayer20DollarsMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] > 1.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayer20DollarsMusic[client][0]) EmitSoundToClient(client, g_strPlayer20DollarsMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); + + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOut20DollarsMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayer20DollarsMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_20dollars_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayer20DollarsMusic[client], false)) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayer20DollarsMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] < 0.0) g_flPlayer20DollarsMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayer20DollarsMusicVolumes[client][iBossIndex]); + + if (g_flPlayer20DollarsMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayer20DollarsMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInAlertMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerAlertMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerAlertMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerAlertMusic[client][0]) EmitSoundToClient(client, g_strPlayerAlertMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); + + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutAlertMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerAlertMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_alert_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerAlertMusic[client], false)) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerAlertMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerAlertMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerAlertMusicVolumes[client][iBossIndex]); + + if (g_flPlayerAlertMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerAlertMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInChaseMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerChaseMusicVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerChaseMusic[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusic[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeInChaseMusicSee(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] += 0.07; + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] > 1.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 1.0; + + if (g_strPlayerChaseMusicSee[client][0]) EmitSoundToClient(client, g_strPlayerChaseMusicSee[client], _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] >= 1.0) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutChaseMusic(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_music", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerChaseMusic[client], false)) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerChaseMusicVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerChaseMusicTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +public Action:Timer_PlayerFadeOutChaseMusicSee(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return Plugin_Stop; + + new iBossIndex = -1; + for (new i = 0; i < MAX_BOSSES; i++) + { + if (g_hPlayerChaseMusicSeeTimer[client][i] == timer) + { + iBossIndex = i; + break; + } + } + + if (iBossIndex == -1) return Plugin_Stop; + + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iBossIndex, sProfile, sizeof(sProfile)); + + decl String:sBuffer[PLATFORM_MAX_PATH]; + GetRandomStringFromProfile(sProfile, "sound_chase_visible", sBuffer, sizeof(sBuffer), 1); + + if (StrEqual(sBuffer, g_strPlayerChaseMusicSee[client], false)) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] -= 0.07; + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] < 0.0) g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] = 0.0; + + if (sBuffer[0]) EmitSoundToClient(client, sBuffer, _, MUSIC_CHAN, _, SND_CHANGEVOL, g_flPlayerChaseMusicSeeVolumes[client][iBossIndex]); + + if (g_flPlayerChaseMusicSeeVolumes[client][iBossIndex] <= 0.0) + { + g_hPlayerChaseMusicSeeTimer[client][iBossIndex] = INVALID_HANDLE; + return Plugin_Stop; + } + + return Plugin_Continue; +} + +stock bool:ClientHasMusicFlag(client, iFlag) +{ + return bool:(g_iPlayerMusicFlags[client] & iFlag); +} + +stock bool:ClientHasMusicFlag2(iValue, iFlag) +{ + return bool:(iValue & iFlag); +} + +stock ClientAddMusicFlag(client, iFlag) +{ + if (!ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] |= iFlag; +} + +stock ClientRemoveMusicFlag(client, iFlag) +{ + if (ClientHasMusicFlag(client, iFlag)) g_iPlayerMusicFlags[client] &= ~iFlag; +} + +// ========================================================== +// MISC FUNCTIONS +// ========================================================== + +// This could be used for entities as well. +stock ClientStopAllSlenderSounds(client, const String:profileName[], const String:sectionName[], iChannel) +{ + if (!client || !IsValidEntity(client)) return; + + if (!IsProfileValid(profileName)) return; + + decl String:buffer[PLATFORM_MAX_PATH]; + + KvRewind(g_hConfig); + if (KvJumpToKey(g_hConfig, profileName)) + { + decl String:s[32]; + + if (KvJumpToKey(g_hConfig, sectionName)) + { + for (new i2 = 1;; i2++) + { + IntToString(i2, s, sizeof(s)); + KvGetString(g_hConfig, s, buffer, sizeof(buffer)); + if (!buffer[0]) break; + + StopSound(client, iChannel, buffer); + } + } + } +} + +stock ClientUpdateListeningFlags(client, bool:bReset=false) +{ + if (!IsClientInGame(client)) return; + + for (new i = 1; i <= MaxClients; i++) + { + if (i == client || !IsClientInGame(i)) continue; + + if (bReset || IsRoundEnding() || GetConVarBool(g_cvAllChat)) + { + SetListenOverride(client, i, Listen_Default); + continue; + } + + new MuteMode:iMuteMode = g_iPlayerPreferences[client][PlayerPreference_MuteMode]; + + if (g_bPlayerEliminated[client]) + { + if (!g_bPlayerEliminated[i]) + { + if (iMuteMode == MuteMode_DontHearOtherTeam) + { + SetListenOverride(client, i, Listen_No); + } + else if (iMuteMode == MuteMode_DontHearOtherTeamIfNotProxy && !g_bPlayerProxy[client]) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + if (!g_bPlayerEliminated[i]) + { + if (g_bSpecialRound && g_iSpecialRoundType == SPECIALROUND_SINGLEPLAYER) + { + if (DidClientEscape(i)) + { + if (!DidClientEscape(client)) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + else + { + if (!DidClientEscape(client)) + { + SetListenOverride(client, i, Listen_No); + } + else + { + SetListenOverride(client, i, Listen_Default); + } + } + } + else + { + new bool:bCanHear = false; + if (GetConVarFloat(g_cvPlayerVoiceDistance) <= 0.0) bCanHear = true; + + if (!bCanHear) + { + decl Float:flMyPos[3], Float:flHisPos[3]; + GetClientEyePosition(client, flMyPos); + GetClientEyePosition(i, flHisPos); + + new Float:flDist = GetVectorDistance(flMyPos, flHisPos); + + if (GetConVarFloat(g_cvPlayerVoiceWallScale) > 0.0) + { + new Handle:hTrace = TR_TraceRayFilterEx(flMyPos, flHisPos, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceRayDontHitCharacters); + new bool:bDidHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bDidHit) + { + flDist *= GetConVarFloat(g_cvPlayerVoiceWallScale); + } + } + + if (flDist <= GetConVarFloat(g_cvPlayerVoiceDistance)) + { + bCanHear = true; + } + } + + if (bCanHear) + { + if (IsClientInGhostMode(i) != IsClientInGhostMode(client) && + DidClientEscape(i) != DidClientEscape(client)) + { + bCanHear = false; + } + } + + if (bCanHear) + { + SetListenOverride(client, i, Listen_Default); + } + else + { + SetListenOverride(client, i, Listen_No); + } + } + } + else + { + SetListenOverride(client, i, Listen_No); + } + } + } +} + +stock ClientShowMainMessage(client, const String:sMessage[], any:...) +{ + decl String:message[512]; + VFormat(message, sizeof(message), sMessage, 3); + + SetHudTextParams(-1.0, 0.4, + 5.0, + 255, + 255, + 255, + 200, + 2, + 1.0, + 0.07, + 2.0); + ShowSyncHudText(client, g_hHudSync, message); +} + +stock ClientResetSlenderStats(client) +{ +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("START ClientResetSlenderStats(%d)", client); +#endif + + g_flPlayerStress[client] = 0.0; + g_flPlayerStressNextUpdateTime[client] = -1.0; + + for (new i = 0; i < MAX_BOSSES; i++) + { + g_bPlayerSeesSlender[client][i] = false; + g_flPlayerSeesSlenderLastTime[client][i] = -1.0; + g_flPlayerSightSoundNextTime[client][i] = -1.0; + } + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 2) DebugMessage("END ClientResetSlenderStats(%d)", client); +#endif +} + +bool:ClientSetQueuePoints(client, iAmount) +{ + if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return false; + g_iPlayerQueuePoints[client] = iAmount; + ClientSaveCookies(client); + return true; +} + +ClientSaveCookies(client) +{ + if (!IsClientConnected(client) || !AreClientCookiesCached(client)) return; + + // Save and reset our queue points. + decl String:s[64]; + Format(s, sizeof(s), "%d ; %d ; %d ; %d ; %d ; %d", g_iPlayerQueuePoints[client], + g_iPlayerPreferences[client][PlayerPreference_ShowHints], + g_iPlayerPreferences[client][PlayerPreference_MuteMode], + g_iPlayerPreferences[client][PlayerPreference_FilmGrain], + g_iPlayerPreferences[client][PlayerPreference_EnableProxySelection], + g_iPlayerPreferences[client][PlayerPreference_GhostOverlay]); + + SetClientCookie(client, g_hCookie, s); +} + +stock ClientViewPunch(client, const Float:angleOffset[3]) +{ + if (g_offsPlayerPunchAngleVel == -1) return; + + decl Float:flOffset[3]; + for (new i = 0; i < 3; i++) flOffset[i] = angleOffset[i]; + ScaleVector(flOffset, 20.0); + + /* + if (!IsFakeClient(client)) + { + // Latency compensation. + new Float:flLatency = GetClientLatency(client, NetFlow_Outgoing); + new Float:flLatencyCalcDiff = 60.0 * Pow(flLatency, 2.0); + + for (new i = 0; i < 3; i++) flOffset[i] += (flOffset[i] * flLatencyCalcDiff); + } + */ + + decl Float:flAngleVel[3]; + GetEntDataVector(client, g_offsPlayerPunchAngleVel, flAngleVel); + AddVectors(flAngleVel, flOffset, flOffset); + SetEntDataVector(client, g_offsPlayerPunchAngleVel, flOffset, true); +} + +public Action:Hook_ConstantGlowSetTransmit(ent, other) +{ + if (!g_bEnabled) return Plugin_Continue; + + new iOwner = -1; + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (EntRefToEntIndex(g_iPlayerConstantGlowEntity[i]) == ent) + { + iOwner = i; + break; + } + } + + if (iOwner != -1) + { + if (!IsPlayerAlive(iOwner) || g_bPlayerEliminated[iOwner]) return Plugin_Handled; + if (!IsPlayerAlive(other) || (!g_bPlayerProxy[other] && !IsClientInGhostMode(other))) return Plugin_Handled; + } + + return Plugin_Continue; +} + +stock ClientSetFOV(client, iFOV) +{ + SetEntData(client, g_offsPlayerFOV, iFOV); + SetEntData(client, g_offsPlayerDefaultFOV, iFOV); +} + +stock TF2_GetClassName(TFClassType:iClass, String:sBuffer[], sBufferLen) +{ + switch (iClass) + { + case TFClass_Scout: strcopy(sBuffer, sBufferLen, "scout"); + case TFClass_Sniper: strcopy(sBuffer, sBufferLen, "sniper"); + case TFClass_Soldier: strcopy(sBuffer, sBufferLen, "soldier"); + case TFClass_DemoMan: strcopy(sBuffer, sBufferLen, "demoman"); + case TFClass_Heavy: strcopy(sBuffer, sBufferLen, "heavyweapons"); + case TFClass_Medic: strcopy(sBuffer, sBufferLen, "medic"); + case TFClass_Pyro: strcopy(sBuffer, sBufferLen, "pyro"); + case TFClass_Spy: strcopy(sBuffer, sBufferLen, "spy"); + case TFClass_Engineer: strcopy(sBuffer, sBufferLen, "engineer"); + default: strcopy(sBuffer, sBufferLen, ""); + } +} + +#define EF_DIMLIGHT (1 << 2) + +stock ClientSDKFlashlightTurnOn(client) +{ + if (!IsValidClient(client)) return; + + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (iEffects & EF_DIMLIGHT) return; + + iEffects |= EF_DIMLIGHT; + + SetEntProp(client, Prop_Send, "m_fEffects", iEffects); +} + +stock ClientSDKFlashlightTurnOff(client) +{ + if (!IsValidClient(client)) return; + + new iEffects = GetEntProp(client, Prop_Send, "m_fEffects"); + if (!(iEffects & EF_DIMLIGHT)) return; + + iEffects &= ~EF_DIMLIGHT; + + SetEntProp(client, Prop_Send, "m_fEffects", iEffects); +} + +stock bool:IsPointVisibleToAPlayer(const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false) +{ + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (IsPointVisibleToPlayer(i, pos, bCheckFOV, bCheckBlink)) return true; + } + + return false; +} + +stock bool:IsPointVisibleToPlayer(client, const Float:pos[3], bool:bCheckFOV=true, bool:bCheckBlink=false, bool:bCheckEliminated=true) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || IsClientInGhostMode(client)) return false; + + if (bCheckEliminated && g_bPlayerEliminated[client]) return false; + + if (bCheckBlink && IsClientBlinking(client)) return false; + + decl Float:eyePos[3]; + GetClientEyePosition(client, eyePos); + + // Check fog, if we can. + if (g_offsPlayerFogCtrl != -1 && g_offsFogCtrlEnable != -1 && g_offsFogCtrlEnd != -1) + { + new iFogEntity = GetEntDataEnt2(client, g_offsPlayerFogCtrl); + if (IsValidEdict(iFogEntity)) + { + if (GetEntData(iFogEntity, g_offsFogCtrlEnable) && + GetVectorDistance(eyePos, pos) >= GetEntDataFloat(iFogEntity, g_offsFogCtrlEnd)) + { + return false; + } + } + } + + new Handle:hTrace = TR_TraceRayFilterEx(eyePos, pos, CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_MIST, RayType_EndPoint, TraceRayDontHitCharactersOrEntity, client); + new bool:bHit = TR_DidHit(hTrace); + CloseHandle(hTrace); + + if (bHit) return false; + + if (bCheckFOV) + { + decl Float:eyeAng[3], Float:reqVisibleAng[3]; + GetClientEyeAngles(client, eyeAng); + + new Float:flFOV = float(g_iPlayerDesiredFOV[client]); + SubtractVectors(pos, eyePos, reqVisibleAng); + GetVectorAngles(reqVisibleAng, reqVisibleAng); + + new Float:difference = FloatAbs(AngleDiff(eyeAng[0], reqVisibleAng[0])) + FloatAbs(AngleDiff(eyeAng[1], reqVisibleAng[1])); + if (difference > ((flFOV * 0.5) + 10.0)) return false; + } + + return true; +} + +public Action:Timer_ClientPostWeapons(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (!IsPlayerAlive(client)) return; + + if (timer != g_hPlayerPostWeaponsTimer[client]) return; + +#if defined DEBUG + if (GetConVarInt(g_cvDebugDetail) > 0) + { + DebugMessage("START Timer_ClientPostWeapons(%d)", client); + } + + new iOldWeaponItemIndexes[6] = { -1, ... }; + new iNewWeaponItemIndexes[6] = { -1, ... }; + + for (new i = 0; i <= 5; i++) + { + new iWeapon = GetPlayerWeaponSlot(client, i); + if (!IsValidEdict(iWeapon)) continue; + + iOldWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + } + +#endif + + new bool:bRemoveWeapons = true; + new bool:bRestrictWeapons = true; + + if (IsRoundEnding()) + { + if (!g_bPlayerEliminated[client]) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + } + + // pvp + if (IsClientInPvP(client)) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + + if (IsRoundInWarmup()) + { + bRemoveWeapons = false; + bRestrictWeapons = false; + } + + if (IsClientInGhostMode(client)) + { + bRemoveWeapons = true; + } + + if (bRemoveWeapons) + { + if(!IsClientInGhostMode(client)) + { + for (new i = 0; i <= 5; i++) + { + TF2_RemoveWeaponSlotAndWearables(client, i); + if (i == TFWeaponSlot_Melee) + { + // Give scout bat to every player to fix camera displacement. + new iNewWeapon = TF2Items_GiveNamedItem(client, g_hSDKWeaponBat); + EquipPlayerWeapon(client, iNewWeapon); + } + } + } + + new ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_weapon_builder")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + + ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_wearable_demoshield")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + + ClientSwitchToWeaponSlot(client, TFWeaponSlot_Melee); + + Hide_Weapon(client); + } else { + Show_Weapon(client); + } + + if (bRestrictWeapons) + { + new iHealth = GetEntProp(client, Prop_Send, "m_iHealth"); + + if (g_hRestrictedWeaponsConfig != INVALID_HANDLE) + { + new TFClassType:iPlayerClass = TF2_GetPlayerClass(client); + new Handle:hItem = INVALID_HANDLE; + + new iWeapon = INVALID_ENT_REFERENCE; + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + + if (IsValidEdict(iWeapon)) + { + if (IsWeaponRestricted(iPlayerClass, GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"))) + { + hItem = INVALID_HANDLE; + TF2_RemoveWeaponSlotAndWearables(client, iSlot); + + switch (iSlot) + { + case TFWeaponSlot_Primary: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponScattergun; + case TFClass_Sniper: hItem = g_hSDKWeaponSniperRifle; + case TFClass_Soldier: hItem = g_hSDKWeaponRocketLauncher; + case TFClass_DemoMan: hItem = g_hSDKWeaponGrenadeLauncher; + case TFClass_Heavy: hItem = g_hSDKWeaponMinigun; + case TFClass_Medic: hItem = g_hSDKWeaponSyringeGun; + case TFClass_Pyro: hItem = g_hSDKWeaponFlamethrower; + case TFClass_Spy: hItem = g_hSDKWeaponRevolver; + case TFClass_Engineer: hItem = g_hSDKWeaponShotgunPrimary; + } + } + case TFWeaponSlot_Secondary: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponPistolScout; + case TFClass_Sniper: hItem = g_hSDKWeaponSMG; + case TFClass_Soldier: hItem = g_hSDKWeaponShotgunSoldier; + case TFClass_DemoMan: hItem = g_hSDKWeaponStickyLauncher; + case TFClass_Heavy: hItem = g_hSDKWeaponShotgunHeavy; + case TFClass_Medic: hItem = g_hSDKWeaponMedigun; + case TFClass_Pyro: hItem = g_hSDKWeaponShotgunPyro; + case TFClass_Engineer: hItem = g_hSDKWeaponPistol; + } + } + case TFWeaponSlot_Melee: + { + switch (iPlayerClass) + { + case TFClass_Scout: hItem = g_hSDKWeaponBat; + case TFClass_Sniper: hItem = g_hSDKWeaponKukri; + case TFClass_Soldier: hItem = g_hSDKWeaponShovel; + case TFClass_DemoMan: hItem = g_hSDKWeaponBottle; + case TFClass_Heavy: hItem = g_hSDKWeaponFists; + case TFClass_Medic: hItem = g_hSDKWeaponBonesaw; + case TFClass_Pyro: hItem = g_hSDKWeaponFireaxe; + case TFClass_Spy: hItem = g_hSDKWeaponKnife; + case TFClass_Engineer: hItem = g_hSDKWeaponWrench; + } + } + case 4: + { + switch (iPlayerClass) + { + case TFClass_Spy: hItem = g_hSDKWeaponInvis; + } + } + } + + if (hItem != INVALID_HANDLE) + { + new iNewWeapon = TF2Items_GiveNamedItem(client, hItem); + if (IsValidEntity(iNewWeapon)) + { + EquipPlayerWeapon(client, iNewWeapon); + } + } + } + } + } + } + + // Fixes the Pretty Boy's Pocket Pistol glitch. + new iMaxHealth = SDKCall(g_hSDKGetMaxHealth, client); + if (iHealth > iMaxHealth) + { + SetEntProp(client, Prop_Data, "m_iHealth", iMaxHealth); + SetEntProp(client, Prop_Send, "m_iHealth", iMaxHealth); + } + } + + // Change stats on some weapons. + if (!g_bPlayerEliminated[client] || g_bPlayerProxy[client]) + { + new iWeapon = INVALID_ENT_REFERENCE; + decl Handle:hWeapon; + for (new iSlot = 0; iSlot <= 5; iSlot++) + { + iWeapon = GetPlayerWeaponSlot(client, iSlot); + if (!iWeapon || iWeapon == INVALID_ENT_REFERENCE) continue; + + new iItemDef = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + switch (iItemDef) + { + case 214: // Powerjack + { + TF2_RemoveWeaponSlot(client, iSlot); + + hWeapon = PrepareItemHandle("tf_weapon_fireaxe", 214, 0, 0, "180 ; 20.0 ; 206 ; 1.33"); + new iEnt = TF2Items_GiveNamedItem(client, hWeapon); + CloseHandle(hWeapon); + EquipPlayerWeapon(client, iEnt); + } + } + } + } + + // Remove all hats. + if (IsClientInGhostMode(client)) + { + new ent = -1; + while ((ent = FindEntityByClassname(ent, "tf_wearable")) != -1) + { + if (GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity") == client) + { + AcceptEntityInput(ent, "Kill"); + } + } + } + +#if defined DEBUG + for (new i = 0; i <= 5; i++) + { + new iWeapon = GetPlayerWeaponSlot(client, i); + if (!IsValidEdict(iWeapon)) continue; + + iNewWeaponItemIndexes[i] = GetEntProp(iWeapon, Prop_Send, "m_iItemDefinitionIndex"); + } + + if (GetConVarInt(g_cvDebugDetail) > 0) + { + for (new i = 0; i <= 5; i++) + { + DebugMessage("-> slot %d: %d (old: %d)", i, iNewWeaponItemIndexes[i], iOldWeaponItemIndexes[i]); + } + + DebugMessage("END Timer_ClientPostWeapons(%d) -> remove = %d, restrict = %d", client, bRemoveWeapons, bRestrictWeapons); + } +#endif +} + +public Action:Timer_ApplyCustomModel(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + new iMaster = NPCGetFromUniqueID(g_iPlayerProxyMaster[client]); + + if (g_bPlayerProxy[client] && iMaster != -1) + { + decl String:sProfile[SF2_MAX_PROFILE_NAME_LENGTH]; + NPCGetProfile(iMaster, sProfile, sizeof(sProfile)); + + // Set custom model, if any. + decl String:sBuffer[PLATFORM_MAX_PATH]; + decl String:sSectionName[64]; + + decl String:sClassName[64]; + TF2_GetClassName(TF2_GetPlayerClass(client), sClassName, sizeof(sClassName)); + + Format(sSectionName, sizeof(sSectionName), "mod_proxy_%s", sClassName); + if ((GetRandomStringFromProfile(sProfile, sSectionName, sBuffer, sizeof(sBuffer)) && sBuffer[0]) || + (GetRandomStringFromProfile(sProfile, "mod_proxy_all", sBuffer, sizeof(sBuffer)) && sBuffer[0])) + { + SetVariantString(sBuffer); + AcceptEntityInput(client, "SetCustomModel"); + SetEntProp(client, Prop_Send, "m_bUseClassAnimations", true); + } + + if (IsPlayerAlive(client)) + { + // Play any sounds, if any. + if (GetRandomStringFromProfile(sProfile, "sound_proxy_spawn", sBuffer, sizeof(sBuffer)) && sBuffer[0]) + { + new iChannel = GetProfileNum(sProfile, "sound_proxy_spawn_channel", SNDCHAN_AUTO); + new iLevel = GetProfileNum(sProfile, "sound_proxy_spawn_level", SNDLEVEL_NORMAL); + new iFlags = GetProfileNum(sProfile, "sound_proxy_spawn_flags", SND_NOFLAGS); + new Float:flVolume = GetProfileFloat(sProfile, "sound_proxy_spawn_volume", SNDVOL_NORMAL); + new iPitch = GetProfileNum(sProfile, "sound_proxy_spawn_pitch", SNDPITCH_NORMAL); + + EmitSoundToAll(sBuffer, client, iChannel, iLevel, iFlags, flVolume, iPitch); + } + } + } +} + +bool:IsWeaponRestricted(TFClassType:iClass, iItemDef) +{ + if (g_hRestrictedWeaponsConfig == INVALID_HANDLE) return false; + + new bool:bReturn = false; + + decl String:sItemDef[32]; + IntToString(iItemDef, sItemDef, sizeof(sItemDef)); + + KvRewind(g_hRestrictedWeaponsConfig); + if (KvJumpToKey(g_hRestrictedWeaponsConfig, "all")) + { + bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef); + } + + new bool:bFoundSection = false; + KvRewind(g_hRestrictedWeaponsConfig); + + switch (iClass) + { + case TFClass_Scout: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "scout"); + case TFClass_Soldier: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "soldier"); + case TFClass_Sniper: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "sniper"); + case TFClass_DemoMan: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "demoman"); + case TFClass_Heavy: + { + bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavy"); + + if (!bFoundSection) + { + KvRewind(g_hRestrictedWeaponsConfig); + bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "heavyweapons"); + } + } + case TFClass_Medic: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "medic"); + case TFClass_Spy: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "spy"); + case TFClass_Pyro: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "pyro"); + case TFClass_Engineer: bFoundSection = KvJumpToKey(g_hRestrictedWeaponsConfig, "engineer"); + } + + if (bFoundSection) + { + bReturn = bool:KvGetNum(g_hRestrictedWeaponsConfig, sItemDef, bReturn); + } + + return bReturn; +} + +public Action:Timer_RespawnPlayer(Handle:timer, any:userid) +{ + new client = GetClientOfUserId(userid); + if (client <= 0) return; + + if (IsPlayerAlive(client)) return; + + TF2_RespawnPlayer(client); } \ No newline at end of file -- GitLab From 846bb4a04aa7788ae79ad781a334e027ae2a83bd Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 03:20:48 +0300 Subject: [PATCH 3/6] Update ghost mode default state --- addons/sourcemod/scripting/rytp_horror.sp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index 0ad5e9c..90db43e 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -5186,6 +5186,9 @@ public Action:Timer_PlayerSwitchToBlue(Handle:timer, any:userid) if (timer != g_hPlayerSwitchBlueTimer[client]) return; ChangeClientTeam(client, _:TFTeam_Blue); + + ClientSetGhostModeState(client, true); + TF2_RespawnPlayer(client); } public Action:Timer_RoundStart(Handle:timer) -- GitLab From b7f0da326c229e75bcb617f31a73812dc99be5ab Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 03:21:15 +0300 Subject: [PATCH 4/6] Display ghost mode menu on death --- addons/sourcemod/scripting/rytp_horror.sp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index 90db43e..03215b4 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -5061,6 +5061,8 @@ public Event_PlayerDeath(Handle:event, const String:name[], bool:dB) g_hPlayerSwitchBlueTimer[client] = CreateTimer(2.5, Timer_PlayerSwitchToBlue, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); ClientCommand(client, "r_screenoverlay %s", STATIC_OVERLAY); EmitSoundToClient(client, STATIC_SOUND, _, MUSIC_CHAN, SNDLEVEL_NONE); + + DisplayMenu(g_hMenuGhostMode, client, 15); } } else -- GitLab From 496c80d3340ebceacedc34a63f3d77d343c7a3a3 Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 04:37:47 +0300 Subject: [PATCH 5/6] Hide camera overlay in third person --- addons/sourcemod/scripting/rytp_horror.sp | 41 ++++++++++++++++++- .../sourcemod/scripting/rytp_horror/client.sp | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index 03215b4..398fe6a 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -250,6 +250,7 @@ new g_iPlayerPageCount[MAXPLAYERS + 1]; new g_iPlayerQueuePoints[MAXPLAYERS + 1]; new bool:g_bPlayerPlaying[MAXPLAYERS + 1]; new Handle:g_hPlayerOverlayCheck[MAXPLAYERS + 1]; +int g_nForceTauntCam[MAXPLAYERS + 1]; new Handle:g_hPlayerSwitchBlueTimer[MAXPLAYERS + 1]; @@ -305,6 +306,7 @@ new g_iPlayer20DollarsMusicMaster[MAXPLAYERS + 1] = { -1, ... }; // Player overlay data new Handle:g_hOverlayUpdateTimer[MAXPLAYERS + 1]; +new Handle:g_hOverlayUpdateVisibility[MAXPLAYERS + 1]; new SF2RoundState:g_iRoundState = SF2RoundState_Invalid; new bool:g_bRoundGrace = false; @@ -3041,6 +3043,7 @@ public OnClientPutInServer(client) g_bPlayerChoseTeam[client] = false; g_bPlayerPlayedSpecialRound[client] = true; g_bPlayerPlayedNewBossRound[client] = true; + g_nForceTauntCam[client] = 0; g_iPlayerPreferences[client][PlayerPreference_PvPAutoSpawn] = false; g_iPlayerPreferences[client][PlayerPreference_ProjectedFlashlight] = false; @@ -3142,6 +3145,8 @@ public OnClientDisconnect(client) { DestroySpriteOverlay(client); g_hOverlayUpdateTimer[client] = INVALID_HANDLE; + CloseHandle(g_hOverlayUpdateVisibility[client]); + g_hOverlayUpdateVisibility[client] = INVALID_HANDLE; if (!g_bEnabled) return; @@ -3150,6 +3155,7 @@ public OnClientDisconnect(client) #endif g_bPlayerEscaped[client] = false; + g_nForceTauntCam[client] = 0; // Save and reset settings for the next client. ClientSaveCookies(client); @@ -4496,6 +4502,7 @@ public Event_PlayerTeam(Handle:event, const String:name[], bool:dB) g_bPlayerPlaying[client] = false; g_bPlayerEliminated[client] = true; g_bPlayerEscaped[client] = false; + g_nForceTauntCam[client] = GetEntProp(client, Prop_Send, "m_nForceTauntCam"); ClientSetGhostModeState(client, false); @@ -4653,6 +4660,8 @@ public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) new client = GetClientOfUserId(GetEventInt(event, "userid")); if (client <= 0) return; + g_nForceTauntCam[client] = GetEntProp(client, Prop_Send, "m_nForceTauntCam"); + #if defined DEBUG if (GetConVarInt(g_cvDebugDetail) > 0) DebugMessage("EVENT START: Event_PlayerSpawn(%d)", client); #endif @@ -4665,6 +4674,8 @@ public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; g_hOverlayUpdateTimer[client] = INVALID_HANDLE; + CloseHandle(g_hOverlayUpdateVisibility[client]); + g_hOverlayUpdateVisibility[client] = INVALID_HANDLE; g_iGhostNextHelpPhrase[client] = 0; if (IsPlayerAlive(client) && IsClientParticipating(client)) @@ -4750,7 +4761,33 @@ public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) #endif } -public Action:Timer_UpdateOverlayTime(Handle:timer, any:userid) +public Action:Timer_UpdateOverlayVisibility(Handle timer, int client) +{ + if (!IsValidClient(client)) return; + + int nForceTauntCam = GetEntProp(client, Prop_Send, "m_nForceTauntCam"); + + if(nForceTauntCam == g_nForceTauntCam[client]) return; + + g_nForceTauntCam[client] = nForceTauntCam; + + PrintToChat(client, "Update overlay visibility: %d", g_nForceTauntCam[client]); + + for(new i = 0; i < sizeof(g_iOverlayRef[]); i++) + { + new sprite = INVALID_HANDLE; + if(Overlay_Layer_Exists(client, i, sprite)) + { + if (g_nForceTauntCam[client]) + { + Overlay_Hide(sprite); + } else + { + Overlay_Show(sprite); + } + } + } +} { new client = GetClientOfUserId(userid); if (client <= 0) return; @@ -4871,7 +4908,7 @@ public Action:Timer_CreateSpriteOverlay(Handle:timer, any:userid) //PrintToServer("%s %d %s %s %s", g_sCameraYear, strlen(g_sCameraYear), g_sCameraYear[strlen(g_sCameraYear)-1], g_sCameraYear[strlen(g_sCameraYear)-2], g_sCameraYear[strlen(g_sCameraYear)-3]); Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit6), 8.0); Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit7), 6.0); - g_hOverlayUpdateTimer[client] = CreateTimer(1.2, Timer_UpdateOverlayTime, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); + g_hOverlayUpdateVisibility[client] = CreateTimer(0.1, Timer_UpdateOverlayVisibility, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); } public Action:Timer_IntroBlackOut(Handle:timer, any:userid) diff --git a/addons/sourcemod/scripting/rytp_horror/client.sp b/addons/sourcemod/scripting/rytp_horror/client.sp index 4e231e5..bbcbb63 100644 --- a/addons/sourcemod/scripting/rytp_horror/client.sp +++ b/addons/sourcemod/scripting/rytp_horror/client.sp @@ -393,7 +393,7 @@ public Hook_ClientPreThink(client) public Action:Hook_ClientSetTransmit(client, other) { if (!g_bEnabled) return Plugin_Continue; - + if (other != client) { if (IsClientInGhostMode(client) && !IsClientInGhostMode(other)) return Plugin_Handled; -- GitLab From 94409004096166d9d5f81b8a2a91f5159693564c Mon Sep 17 00:00:00 2001 From: Alexey <lexuzieel@gmail.com> Date: Wed, 14 Aug 2019 04:38:39 +0300 Subject: [PATCH 6/6] Update overlay update timer code Add handle close functions --- addons/sourcemod/scripting/rytp_horror.sp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/sourcemod/scripting/rytp_horror.sp b/addons/sourcemod/scripting/rytp_horror.sp index 398fe6a..a03053b 100644 --- a/addons/sourcemod/scripting/rytp_horror.sp +++ b/addons/sourcemod/scripting/rytp_horror.sp @@ -3144,6 +3144,8 @@ public OnClientGetDesiredFOV(QueryCookie:cookie, client, ConVarQueryResult:resul public OnClientDisconnect(client) { DestroySpriteOverlay(client); + + CloseHandle(g_hOverlayUpdateTimer[client]); g_hOverlayUpdateTimer[client] = INVALID_HANDLE; CloseHandle(g_hOverlayUpdateVisibility[client]); g_hOverlayUpdateVisibility[client] = INVALID_HANDLE; @@ -4659,7 +4661,7 @@ public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) new client = GetClientOfUserId(GetEventInt(event, "userid")); if (client <= 0) return; - + g_nForceTauntCam[client] = GetEntProp(client, Prop_Send, "m_nForceTauntCam"); #if defined DEBUG @@ -4673,6 +4675,7 @@ public Event_PlayerSpawn(Handle:event, const String:name[], bool:dB) g_hPlayerPostWeaponsTimer[client] = INVALID_HANDLE; + CloseHandle(g_hOverlayUpdateTimer[client]); g_hOverlayUpdateTimer[client] = INVALID_HANDLE; CloseHandle(g_hOverlayUpdateVisibility[client]); g_hOverlayUpdateVisibility[client] = INVALID_HANDLE; @@ -4788,9 +4791,11 @@ public Action:Timer_UpdateOverlayVisibility(Handle timer, int client) } } } + +public Action:Timer_UpdateOverlayTime(Handle timer, int client) { - new client = GetClientOfUserId(userid); - if (client <= 0) return; + if (!IsValidClient(client)) return; + new String:Time[64], String:DigitBuffer[2]; FormatTime(Time, sizeof(Time), "%d"); // Day Format(DigitBuffer, 2, "%s", Time[0]); @@ -4908,6 +4913,7 @@ public Action:Timer_CreateSpriteOverlay(Handle:timer, any:userid) //PrintToServer("%s %d %s %s %s", g_sCameraYear, strlen(g_sCameraYear), g_sCameraYear[strlen(g_sCameraYear)-1], g_sCameraYear[strlen(g_sCameraYear)-2], g_sCameraYear[strlen(g_sCameraYear)-3]); Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit6), 8.0); Overlay_Frame(OverlayRef_ByLayer(client, _:DateDigit7), 6.0); + g_hOverlayUpdateTimer[client] = CreateTimer(1.2, Timer_UpdateOverlayTime, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); g_hOverlayUpdateVisibility[client] = CreateTimer(0.1, Timer_UpdateOverlayVisibility, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); } -- GitLab