/* * ============================================================================ * * Zombie:Reloaded * * File: immunityhandler.inc * Type: Core module * Description: Manages infection immunity modes for every player. * * Copyright (C) 2009-2013 Greyscale, Richard Helgeby * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * ============================================================================ */ /** * Maximum delay of infection. */ #define IMMUNITY_MAX_DELAY 300 /** * Maximum shield duration. */ #define IMMUNITY_MAX_SHIELD_TIME 300 /** * Timers for handling timed immunity actions. */ new Handle:PlayerImmunityTimer[MAXPLAYERS + 1] = {INVALID_HANDLE, ...}; /** * Remaining time of timed immunity actions. */ new PlayerImmunityDuration[MAXPLAYERS + 1] = {-1, ...}; /** * Cached attacker index for delayed infections, if available. */ new PlayerImmunityAttacker[MAXPLAYERS + 1] = {0, ...}; /** * Timestamp of last action. Usage depends on mode (cooldown, etc). */ new PlayerImmunityLastUse[MAXPLAYERS + 1] = {0, ...}; /** * Whether the player has passed a threshold (infect mode). */ new bool:PlayerImmunityThresholdPassed[MAXPLAYERS + 1] = {false, ...}; /*____________________________________________________________________________*/ /** * Console commands are being created. */ ImmunityOnCommandsCreate() { RegConsoleCmd(SAYHOOKS_KEYWORD_ZSHIELD, Command_DeployShield, "Deploy the shield, if available."); } /*____________________________________________________________________________*/ /** * Client executed the deploy shield command. * * @param client Client index. * @param argc Number of arguments. */ public Action:Command_DeployShield(client, argc) { // Block console. if (ZRIsConsole(client)) { TranslationPrintToServer("Must be player"); return Plugin_Handled; } // Attempt to deploy shield. ImmunityDeployShield(client); return Plugin_Handled; } /*____________________________________________________________________________*/ /** * Handles immunity when a client is about to be infected. This function may * delay or block infection according to the immunity mode class settings. * * @param client Client that's being infected. * @param attacker Attacker client (zombie). * * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ bool:ImmunityOnClientInfect(client, attacker) { // Get immunity mode from client class. new ImmunityMode:mode = ClassGetImmunityMode(client); // Check mode. switch(mode) { case Immunity_None: { // Instant infection. return false; } case Immunity_Kill: { // Block infection. Damage is increased in ImmunityOnClientDamage. return true; } case Immunity_Full: { // Full immunity, do nothing. return true; } case Immunity_Infect: { return ImmunityInfectModeHandler(client); } case Immunity_Delay: { return ImmunityDelayModeHandler(client, attacker); } case Immunity_Shield: { return ImmunityShieldModeHandler(client); } } // Current mode doesn't apply to infection. return false; } /*____________________________________________________________________________*/ /** * TraceAttack hook. * * Returns whether attacker damage should be blocked. If damage is blocked this * module will handle it. * * @param client Client index. * @param attacker Attacker client, if any. * @param inflictor The entity index of the inflictor. * @param damage Damage received by client. * * @return True if damage should be blocked, false otherwise. */ bool ImmunityOnClientTraceAttack(int client, int &attacker, int &inflictor, float &damage, int &damagetype, int &ammotype, int hitgroup) { // Check if there is no attacker (world damage). if (!ZRIsClientValid(attacker)) { // Allow damage. return false; } // Get immunity mode from client class. new ImmunityMode:mode = ClassGetImmunityMode(client); // Check mode. switch(mode) { case Immunity_Full: { // Block damage (implies blocking knock back on zombies). return true; } case Immunity_Infect: { // Client must be human. if (InfectIsClientInfected(client)) { // Allow damage. return false; } // Check if damage give HP below the infection threshold. if (ImmunityBelowInfectThreshold(client, damage)) { PlayerImmunityThresholdPassed[client] = true; } else { PlayerImmunityThresholdPassed[client] = false; } } case Immunity_Damage: { // Client must be zombie. if (!InfectIsClientInfected(client)) { // Allow damage. return false; } // Get attacker weapon. decl String:weapon[32]; weapon[0] = 0; if (damagetype == DMG_BLAST) { // Most likely a HE grenade. GetClientWeapon can't be used if // the attacker throw grenades. The attacker may switch weapon // before the grenade explodes. strcopy(weapon, sizeof(weapon), "hegrenade"); } else { GetClientWeapon(attacker, weapon, sizeof(weapon)); } // Since damage is blocked, trigger knock back hurt event manually. KnockbackOnClientHurt(client, attacker, inflictor, weapon, hitgroup, damage, damagetype, -1, 0); // Block damage from attacker. return true; } case Immunity_Shield: { // Client must be human. if (InfectIsClientInfected(client)) { // Allow damage. return false; } // Check if shield is active. if (PlayerImmunityTimer[client] != INVALID_HANDLE) { // Block damage for humans with shield enabled (preventing // zombies from stabbing them to death). return true; } } } // Allow damage. return false; } /*____________________________________________________________________________*/ /** * TakeDamage hook. * * Blocks or modifies damage in certain situations. * * @param client Client index. * @param attacker Attacker client, if any. * @param damage Damage received by client. * @param weapon Weapon entity. * * @return Plugin_Handled if damage was blocked, Plugin_Changed if * damage was modified, Plugin_Continue otherwise. */ Action:ImmunityOnClientDamage(client, attacker, &Float:damage) { // Check if there is no attacker (world damage). if (!ZRIsClientValid(attacker)) { // Allow damage. return Plugin_Continue; } // Check if spawn protection is on. if (g_bInfectImmune[client]) { // Block damage. return Plugin_Handled; } // Get immunity mode from client class. new ImmunityMode:mode = ClassGetImmunityMode(client); switch(mode) { case Immunity_Kill: { // Client must be human and attacker must be zombie. if (InfectIsClientInfected(client) || !InfectIsClientInfected(attacker)) { // Don't modify damage. return Plugin_Continue; } // A zombie is attacking a human in kill immunity mode. Increase // damage so human will be instantly killed. (Using a high damage // value in case the human class has a lot of HP.) damage = 60000.0; // Update score and health gain. InfectUpdateScore(attacker, client); // Damage was changed. return Plugin_Changed; } case Immunity_Infect: { // Prevent humans with low HP from dying when a zombie is // attacking, and stab to death is disabled (threshold above zero). if (ImmunityBelowInfectThreshold(client, damage)) { // Fake hurt event because it's not triggered when the damage // was removed (because no one is actually hurt). InfectOnClientHurt(client, attacker, "knife"); // Block damage to prevent player from dying. return Plugin_Handled; } } case Immunity_Delay: { // Fake hurt event because it's not triggered when the damage // was removed (because no one is actually hurt). This event must // still be triggered so that subsequent attacks are registered, // without dealing any damage. InfectOnClientHurt(client, attacker, "knife"); // Block damage to prevent player from dying. return Plugin_Handled; } } // Allow damage. return Plugin_Continue; } /*____________________________________________________________________________*/ /** * Handles infect mode immunity. * * Allow humans to receive damage from zombies until HP is below a certain * threshold. If the threshold is zero, never infect. * * @param client Client that's being infected. * @param attacker Attacker client (zombie). * * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ bool:ImmunityInfectModeHandler(client) { // Note: ImmunityOnClientDamage and ImmunityOnClientTraceAttack hook into // the damage module to prevent humans with low HP from dying when // they're not supposed to. new threshold = ClassGetImmunityAmount(client); // Check if infection is disabled. if (threshold == 0) { // Infection is handled here: blocked. return true; } if (PlayerImmunityThresholdPassed[client]) { // Client HP below threshold, allow instant infection. return false; } return true; } /*____________________________________________________________________________*/ /** * Handles delayed infections. * * @param client Client that's being infected. * * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ bool:ImmunityDelayModeHandler(client, attacker) { // Check if an infection is in progress if (PlayerImmunityTimer[client] != INVALID_HANDLE) { // Additional attacks while a delayed infection is in progress will // speedup the infection. // Get reduction amount for subsequent zombie attack. new reduction = ClassGetImmunityCooldown(client); if (reduction > 0) { // Reduce duration. Add one because the timer handler itself reduce // duration by one. PlayerImmunityDuration[client] -= reduction + 1; // Note: This feature can be used to trigger an instant infection // when a human receive a second attack, by setting the // reduction value high enough. // Trigger timer event to reduce delay and infect faster. ImmunityDelayTimerHandler(PlayerImmunityTimer[client], client); } // Block infection. return true; } // Start a delayed infection. Initialize duration and attacker. PlayerImmunityDuration[client] = ClassGetImmunityAmount(client); PlayerImmunityAttacker[client] = attacker; // Create repated 1-second timer for handling the countdown. PlayerImmunityTimer[client] = CreateTimer(1.0, ImmunityDelayTimerHandler, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); // Block infection. return true; } /*____________________________________________________________________________*/ /** * Delayed infection timer handler. Handles countdown and infection when time * is up. */ public Action:ImmunityDelayTimerHandler(Handle:timer, any:client) { // Verify that client is still connected and alive. if (!IsClientInGame(client) || !IsPlayerAlive(client)) { // Client disconnected or died. Abort immunity action. ImmunityAbortHandler(client); return Plugin_Stop; } // Reduce duration. PlayerImmunityDuration[client] -= 1; // Check if time is up. if (PlayerImmunityDuration[client] <= 0) { // Get attacker before cleaning up. new attacker = PlayerImmunityAttacker[client]; // Time is up. Reset data. PlayerImmunityDuration[client] = 0; ImmunityAbortHandler(client); // Infect client. Give credit to the stored attacker. InfectHumanToZombie(client, attacker); return Plugin_Stop; } return Plugin_Continue; } /*____________________________________________________________________________*/ /** * Handles shield mode immunity when client is about to become infected. * * Zombies will get a shield against knock back, while humans become immune of * infections. * * @param client Client deploying shield. * * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ bool:ImmunityShieldModeHandler(client) { // Check if shield is active. if (PlayerImmunityTimer[client] != INVALID_HANDLE) { // Block infection. return true; } // Shield is not active, allow infection. return false; } /*____________________________________________________________________________*/ /** * Attempts to deploy the shield. * * @param client Client index. */ ImmunityDeployShield(client) { // Check if shield is available. if (!ImmunityCanDeployShield(client)) { // Not available. return; } // Deploy the shield. PlayerImmunityDuration[client] = ClassGetImmunityAmount(client); PlayerImmunityLastUse[client] = GetTime(); PlayerImmunityTimer[client] = CreateTimer(1.0, ImmunityShieldTimerHandler, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); // Trigger initial countdown. ImmunityShieldTimerHandler(PlayerImmunityTimer[client], client); } /*____________________________________________________________________________*/ /** * Shield timer handler. Handles countdown and shield removal when time is up. */ public Action:ImmunityShieldTimerHandler(Handle:timer, any:client) { // Verify that client is still connected and alive. if (!IsClientInGame(client) || !IsPlayerAlive(client)) { // Client disconnected or died. Abort immunity action. ImmunityAbortHandler(client); return Plugin_Stop; } // Reduce duration. PlayerImmunityDuration[client] -= 1; // Print remaining shield time. TranslationPrintCenterText(client, "Immunity Shield Time Left", PlayerImmunityDuration[client]); // Check if time is up. if (PlayerImmunityDuration[client] <= 0) { // Time is up. Reset data, but not last use timestamp. ImmunityAbortHandler(client, false); return Plugin_Stop; } return Plugin_Continue; } /*____________________________________________________________________________*/ /** * Aborts any immunity mode in action (shields, delays, etc.). Resets values. * * @param client Client that's aborting immunity mode actions. * @param resetLastUse Reset timestamp of last use. This will reset cooldown. */ ImmunityAbortHandler(client, bool:resetLastUse = true) { // Stop timer, if running. ZREndTimer(PlayerImmunityTimer[client]); // Reset data. PlayerImmunityDuration[client] = -1; PlayerImmunityAttacker[client] = 0; PlayerImmunityThresholdPassed[client] = false; if (resetLastUse) { PlayerImmunityLastUse[client] = 0; } } /*____________________________________________________________________________*/ /** * Aborts all immunity modes in action. * * @param resetLastUse Reset timestamp of last use. This will reset cooldown. */ ImmunityAbortAll(bool:resetLastUse = true) { for (new client = 0; client < MAXPLAYERS + 1; client++) { ImmunityAbortHandler(resetLastUse); } } /*____________________________________________________________________________*/ /** * Client is about to receive knock back. * * @param Client that's receiving knock back. * * @return True if knock back should be blocked, false otherwise. */ bool:ImmunityOnClientKnockBack(client) { // Knock back filter is currently only used in shield mode. if (ClassGetImmunityMode(client) == Immunity_Shield) { // Client must be zombie. (In case a future change allow knock back // on humans.) if (!InfectIsClientInfected(client)) { // Client is human, allow knock back. return false; } // Block knock back if shield is deployed. if (PlayerImmunityTimer[client] != INVALID_HANDLE) { // Block knock back. return true; } } // Allow knock back. return false; } /*____________________________________________________________________________*/ /** * Client was infected. */ ImmunityOnClientInfected(client) { // In case client was infected through an admin command or mother zombie // selection, abort other actions in progress. ImmunityAbortHandler(client); } /*____________________________________________________________________________*/ /** * Client was turned back into a human. */ ImmunityOnClientHuman(client) { ImmunityAbortHandler(client); } /*____________________________________________________________________________*/ /** * Client died. */ ImmunityOnClientDeath(client) { ImmunityAbortHandler(client, false); } /*____________________________________________________________________________*/ /** * Client connected to the server. */ ImmunityClientInit(client) { ImmunityAbortHandler(client); } /*____________________________________________________________________________*/ /** * Client spawned. */ ImmunityClientSpawn(client) { ImmunityAbortHandler(client, false); } /*____________________________________________________________________________*/ /** * Client disconnected. */ ImmunityOnClientDisconnect(client) { ImmunityAbortHandler(client); // Loop through attacker cache and remove client (set to 0). for (new victim = 0; victim < sizeof(PlayerImmunityAttacker); victim++) { if (PlayerImmunityAttacker[victim] == client) { // The victim was attacked by this client, but the client is // disconnecting now. Reset the attacker index to the world index. PlayerImmunityAttacker[victim] = 0; } } } /*____________________________________________________________________________*/ /** * Client changed team. */ ImmunityOnClientTeam(client) { ImmunityAbortHandler(client); } /*____________________________________________________________________________*/ /** * Round ended. */ ImmunityOnRoundEnd() { ImmunityAbortAll(); } /*____________________________________________________________________________*/ /** * Map ended. */ ImmunityOnMapEnd() { ImmunityAbortAll(); } /*____________________________________________________________________________*/ /** * Returns whether the specified damage will take a client's HP below the * infection threshold. Only used by "infect" immunity mode. * * If threshold is disabled (zero) this function will always return false. * * @param client Client index. * @param damage Damage applied to client. * * @return True if client HP go below threshold (immunity_amount) when * applying damage, false if above threshold or if threshold * is disabled (zero). */ bool:ImmunityBelowInfectThreshold(client, Float:damage) { new threshold = ClassGetImmunityAmount(client); new clientHP = GetClientHealth(client); new dmg = RoundToNearest(damage); // Check if the damage go below the HP threshold. Client can only go below // threshold when threshold is enabled (above zero). if (clientHP - dmg <= threshold && threshold > 0) { return true; } return false; } /*____________________________________________________________________________*/ /** * Converts a string to an immunity mode. * * @param mode String to convert. * * @return Immunity mode or Immunity_Invalid on error. */ ImmunityMode:ImmunityStringToMode(const String:mode[]) { if (strlen(mode) == 0) { return Immunity_Invalid; } if (StrEqual(mode, "none", false)) { return Immunity_None; } if (StrEqual(mode, "kill", false)) { return Immunity_Kill; } else if (StrEqual(mode, "full", false)) { return Immunity_Full; } else if (StrEqual(mode, "infect", false)) { return Immunity_Infect; } else if (StrEqual(mode, "damage", false)) { return Immunity_Damage; } else if (StrEqual(mode, "delay", false)) { return Immunity_Delay; } else if (StrEqual(mode, "shield", false)) { return Immunity_Shield; } return Immunity_Invalid; } /*____________________________________________________________________________*/ /** * Converts an immunity mode to a string. * * @param mode Mode to convert. * @param buffer Destination string buffer. * @param maxlen Size of buffer. * * @return Number of cells written. */ ImmunityModeToString(ImmunityMode:mode, String:buffer[], maxlen) { if (mode == Immunity_Invalid) { return 0; } switch (mode) { case Immunity_None: { return strcopy(buffer, maxlen, "none"); } case Immunity_Kill: { return strcopy(buffer, maxlen, "kill"); } case Immunity_Full: { return strcopy(buffer, maxlen, "full"); } case Immunity_Infect: { return strcopy(buffer, maxlen, "infect"); } case Immunity_Damage: { return strcopy(buffer, maxlen, "damage"); } case Immunity_Delay: { return strcopy(buffer, maxlen, "delay"); } case Immunity_Shield: { return strcopy(buffer, maxlen, "shield"); } } return 0; } /*____________________________________________________________________________*/ /** * Returns whether the amount value is valid for the specified mode. * * @param mode Immunity mode to validate against. * @param amount Amount value to test. * * @return True if valid, false otherwise. */ bool:ImmunityIsValidAmount(ImmunityMode:mode, amount) { switch (mode) { case Immunity_Invalid: { return false; } case Immunity_None: { // Immunity mode disabled, amount ignored. return true; } case Immunity_Kill: { // Amount isn't used in this mode. return true; } case Immunity_Full: { // Amount isn't used in this mode. return true; } case Immunity_Infect: { // Check if HP threshold is negative. if (amount < 0) { return false; } // There's no upper limit. If the value is too high it will // overflow and become negative. } case Immunity_Damage: { // Amount isn't used in this mode. return true; } case Immunity_Delay: { if (amount <= 0 || amount > IMMUNITY_MAX_DELAY) { return false; } } case Immunity_Shield: { if (amount <= 0 || amount > IMMUNITY_MAX_SHIELD_TIME) { return false; } } default: { // Invalid mode. return false; } } // Passed. return true; } /*____________________________________________________________________________*/ /** * Returns whether the cooldown value is valid for the specified mode. * * @param mode Immunity mode to validate against. * @param cooldown Cooldown value to test. * * @return True if valid, false otherwise. */ bool:ImmunityIsValidCooldown(ImmunityMode:mode, cooldown) { switch (mode) { case Immunity_Invalid: { return false; } case Immunity_None: { // Immunity mode disabled, amount ignored. return true; } case Immunity_Kill: { // Cooldown isn't used in this mode. return true; } case Immunity_Full: { // Cooldown isn't used in this mode. return true; } case Immunity_Infect: { // Cooldown isn't used in this mode. return true; } case Immunity_Damage: { // Cooldown isn't used in this mode. return true; } case Immunity_Delay: { if (cooldown < 0) { return false; } // No upper limit. It may be intentional to use a high value so that // the section attack will remove all delay and infect instantly. } case Immunity_Shield: { if (cooldown < 0) { return false; } // No upper limit. It may be intentional to use a high value so that // the shield can only be used once per life. } default: { // Invalid mode. return false; } } // Passed. return true; } /*____________________________________________________________________________*/ /** * Returns whether the client is allowed to deploy a shield now. Tests whether * the client has shield immunity mode and whether cooldown is done, or a shield * is already active. * * @param client Client index. * @param printResponse Whether a response message is printed on the * client's screen. * * @return True if shield can be deployed, false otherwise. */ bool:ImmunityCanDeployShield(client, bool:printResponse = true) { // Check immunity mode. if (ClassGetImmunityMode(client) != Immunity_Shield) { if (printResponse) { TranslationPrintToChat(client, "Immunity Shield Not Available"); } return false; } // Check if cooldown is still in progress. new cooldown = ClassGetImmunityCooldown(client); new timeLeft = PlayerImmunityLastUse[client] + cooldown - GetTime(); if (timeLeft > 0) { if (printResponse) { TranslationPrintToChat(client, "Immunity Shield Cooldown", timeLeft); } return false; } // Check if a shield is already deployed. if (PlayerImmunityTimer[client] != INVALID_HANDLE) { return false; } // Humans cannot deploy shield before first zombie. if (!InfectHasZombieSpawned()) { return false; } return true; }