diff --git a/cstrike/addons/sourcemod/configs/zr/playerclasses.txt b/cstrike/addons/sourcemod/configs/zr/playerclasses.txt index b0ec557..bd2644c 100644 --- a/cstrike/addons/sourcemod/configs/zr/playerclasses.txt +++ b/cstrike/addons/sourcemod/configs/zr/playerclasses.txt @@ -32,14 +32,15 @@ // fov number Field of view value. 90 is default. // has_napalm yes/no Allows player to throw napalm grenades. Humans only. // napalm_time decimal Napalm burn duration. Zombies only. -// immunity_mode test How the human is infected (humans only): +// immunity_mode test Special immunity modes. Some modes only works on humans or zombies: // "none" - Instant infection. -// "full" - Immune to infection. Careful with this, it might not be that fun. -// "damage" - Allow zombies to stab humans to death, or below a HP threshold. +// "full" - Completely immune. Humans can't be infected, zombies don't receive damage or knock back. Careful with this, it might not be that fun. +// "infect" - Humans are immune to infections until HP go below a threshold. Threshold at zero enable stabbing to death. +// "damage" - Zombies are immune to damage from humans/grenades, but still vulnerable to knock back. // "delay" - Delay infection for a certain number of seconds. -// "shield" - Allow human to deploy a shield that will give a temporary full immunity against infections. +// "shield" - Shield against infections (humans) or knock back (zombies) for a certain amount of seconds (similar to TF2's übercharge). // immunity_amount number Immunity data value (humans only). Depends on the immunity mode above: -// "damage" - HP threshold. Infection will be allowed when HP go below this value. Zero will enable stabbing to death. +// "infect" - HP threshold. Infection will be allowed when HP go below this value. Zero will enable stabbing to death. // "delay" - Number of seconds the infection is delayed since first hit by a zombie. // "shield" - Number of seconds the shield is active. // immunity_cooldown number Number of seconds of cooldown for temporary immunity effects (currently just shield mode). @@ -90,7 +91,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "2500" @@ -134,7 +136,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "2000" @@ -178,7 +181,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "3500" @@ -222,7 +226,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "4000" @@ -266,7 +271,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "2500" @@ -310,7 +316,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "3500" @@ -360,7 +367,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "no" "health" "100" @@ -404,7 +412,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "200" @@ -448,7 +457,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "200" @@ -492,7 +502,8 @@ // Player behavior "immunity_mode" "none" - "immunity_amount" "0" + "immunity_amount" "1" + "immunity_cooldown" "60" "no_fall_damage" "0" "health" "100" @@ -535,8 +546,9 @@ "napalm_time" "0.0" // Player behavior - "immunity_mode" "none" - "immunity_amount" "0" + "immunity_mode" "infect" // Immune against infection, + "immunity_amount" "10" // when HP is above 10. + "immunity_cooldown" "60" "no_fall_damage" "yes" "health" "100" diff --git a/src/zombiereloaded.sp b/src/zombiereloaded.sp index 09a4f48..3467b90 100644 --- a/src/zombiereloaded.sp +++ b/src/zombiereloaded.sp @@ -38,11 +38,13 @@ #include #define ACTION_CONTINUE Plugin_Continue + #define ACTION_CHANGED Plugin_Changed #define ACTION_HANDLED Plugin_Handled #else #include #define ACTION_CONTINUE ZRTools_Continue + #define ACTION_CHANGED ZRTools_Changed #define ACTION_HANDLED ZRTools_Handled #endif @@ -198,6 +200,7 @@ public OnMapEnd() VolOnMapEnd(); VEffectsOnMapEnd(); ZombieSoundsOnMapEnd(); + ImmunityOnMapEnd(); } /** @@ -260,6 +263,7 @@ public OnClientPutInServer(client) RespawnClientInit(client); ZTeleClientInit(client); ZHPClientInit(client); + ImmunityClientInit(client); } /** @@ -312,6 +316,7 @@ public OnClientDisconnect(client) AntiStickOnClientDisconnect(client); ZSpawnOnClientDisconnect(client); VolOnPlayerDisconnect(client); + ImmunityOnClientDisconnect(client); } /** diff --git a/src/zr/damage.inc b/src/zr/damage.inc index 2123a32..ba1a572 100644 --- a/src/zr/damage.inc +++ b/src/zr/damage.inc @@ -216,6 +216,14 @@ public ZRTools_Action:DamageTraceAttack(client, inflictor, attacker, Float:damag // Here we know that attacker and client are different teams. + // Check if immunity module requires damage to be blocked. + if (ImmunityOnClientTraceAttack(client, attacker, damage, hitgroup, damagetype)) + { + // Block damage. + PrintToChatAll("DamageTraceAttack - Blocking damage."); + return ACTION_HANDLED; + } + // If client is a human, then allow damage. if (InfectIsClientHuman(client)) { @@ -271,6 +279,8 @@ public Action:DamageOnTakeDamage(client, &attacker, &inflictor, &Float:damage, & public ZRTools_Action:DamageOnTakeDamage(client, inflictor, attacker, Float:damage, damagetype, ammotype) #endif { + PrintToChatAll("DamageOnTakeDamage - damage:%f", damage); + // Get classname of the inflictor. decl String:classname[64]; GetEdictClassname(inflictor, classname, sizeof(classname)); @@ -323,12 +333,22 @@ public ZRTools_Action:DamageOnTakeDamage(client, inflictor, attacker, Float:dama return ACTION_CONTINUE; } - // Client is about to be infected, re-add HP so they aren't killed by knife. - new health = GetClientHealth(client); - SetEntityHealth(client, health + RoundToNearest(damage)); + // Check if immunity module blocked the damage. + if (ImmunityOnClientDamage(client, attacker, damage)) + { + return ACTION_HANDLED; + } - // Allow damage. - return ACTION_CONTINUE; + // Client is about to be infected, re-add HP so they aren't killed by + // knife. But only do this when immunity mode is disabled. + if (ClassGetImmunityMode(client) == Immunity_None) + { + new health = GetClientHealth(client); + SetEntityHealth(client, health + RoundToNearest(damage)); + + // Allow damage. + return ACTION_CONTINUE; + } } // Client was damaged by explosion. else if (damagetype & DMG_CSS_BLAST) diff --git a/src/zr/event.inc b/src/zr/event.inc index 4403531..77f8299 100644 --- a/src/zr/event.inc +++ b/src/zr/event.inc @@ -144,6 +144,7 @@ public Action:EventRoundEnd(Handle:event, const String:name[], bool:dontBroadcas RespawnOnRoundEnd(); ZSpawnOnRoundEnd(); VolOnRoundEnd(); + ImmunityOnRoundEnd(); } /** @@ -162,6 +163,7 @@ public Action:EventPlayerTeam(Handle:event, const String:name[], bool:dontBroadc // Forward event to modules. InfectOnClientTeam(index, team); + ImmunityOnClientTeam(index); return Plugin_Handled; } @@ -190,6 +192,7 @@ public Action:EventPlayerSpawn(Handle:event, const String:name[], bool:dontBroad ZTeleOnClientSpawn(index); ZHPOnClientSpawn(index); VolOnPlayerSpawn(index); + ImmunityClientSpawn(index); // Fire post player_spawn event. CreateTimer(0.1, EventPlayerSpawnPost, index); @@ -292,6 +295,7 @@ public Action:EventPlayerDeath(Handle:event, const String:name[], bool:dontBroad ZHPOnClientDeath(index); VolOnPlayerDeath(index); RoundEndOnClientDeath(); + ImmunityOnClientDeath(index); } /** diff --git a/src/zr/immunityhandler.inc b/src/zr/immunityhandler.inc index ef3369a..062aa46 100644 --- a/src/zr/immunityhandler.inc +++ b/src/zr/immunityhandler.inc @@ -36,23 +36,40 @@ new ImmunityMode:PlayerImmunityMode[MAXPLAYERS + 1] = {Immunity_None, ...}; new Handle:PlayerImmunityTimer[MAXPLAYERS + 1] = {INVALID_HANDLE, ...}; /** - * Last time a temporary immunity action was used (currently just shield mode). - * Used with cooldown of actions. + * Remaining time of timed immunity actions. */ -new PlayerImmunityLastUse[MAXPLAYERS + 1] = {-1, ...}; +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, ...}; /** * 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) +bool:ImmunityOnClientInfect(client, attacker) { - // Get immunity data from client class. + PrintToChatAll("ImmunityOnClientInfect(client=%d, attacker=%d)", client, attacker); + + // Get immunity mode from client class. new ImmunityMode:mode = ClassGetImmunityMode(client); // Check mode. @@ -68,35 +85,197 @@ bool:ImmunityOnClientInfect(client) // Full immunity, do nothing. return true; } - case Immunity_Damage: + case Immunity_Infect: { - return ImmunityDamageModeHandler(client); + return ImmunityInfectModeHandler(client, attacker); } case Immunity_Delay: { - return ImmunityDelayModeHandler(client); + return ImmunityDelayModeHandler(client, attacker); } case Immunity_Shield: { return ImmunityShieldModeHandler(client); } - default: - { - ThrowError("Invalid immunity mode. This is a bug in ZR."); - return true; - } } + + // Current mode doesn't apply to infection. + return false; } /** - * Client is about to receive damage (zombie). + * TraceAttack hook. * - * @param Client that's receiving damage. + * Returns whether attacker damage should be blocked. If damage is blocked this + * module will handle it. * - * @return True if damage should be blocked, false otherwise. + * @param client Client index. + * @param attacker Attacker client, if any. + * @param damage Damage received by client. + * @param hitgroup Hitgroup receiving damage. + * + * @return True if damage should be blocked, false otherwise. */ -bool:ImmunityOnClientHurt(client) +bool:ImmunityOnClientTraceAttack(client, attacker, Float:damage, hitgroup, damageType) { + PrintToChatAll("ImmunityOnClientTraceAttack(client=%d, attacker=%d, damage=%f, hitgroup=%d, damageType=%d)", client, attacker, damage, hitgroup, damageType); + + // 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. + 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)) + { + PrintToChatAll("ImmunityOnClientTraceAttack - Threshold passed"); + PlayerImmunityThresholdPassed[client] = true; + } + else + { + PlayerImmunityThresholdPassed[client] = false; + } + + /*new threshold = ClassGetImmunityAmount(client); + new clientHP = GetClientHealth(client); + new dmg = RoundToNearest(damage); + PrintToChatAll("threshold=%d, clientHp=%d", threshold, clientHP); + + // Prevent humans with low HP from dying when a zombie is + // attacking, and stab to death is disabled (threshold above zero). + if (clientHP - dmg <= 0.0 && threshold > 0) + { + // Client is about to be infected. Remove damage, but don't + // block the damage event. The infect module need it to detect + // zombie attack. + damage = 0.0; + + // Client is about to be infected, re-add HP so they aren't + // killed by knife. Don't block the damage, the infect + // module need the damage event. + //PrintToChatAll("Re-add HP."); + //SetEntityHealth(client, clientHP + dmg); + return false; + }*/ + } + case Immunity_Damage: + { + // Client must be zombie. + if (!InfectIsClientInfected(client)) + { + 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, weapon, hitgroup, RoundToNearest(damage)); + + // Block damage from attacker. + return true; + } + case Immunity_Delay: + { + // Client must be human. + if (InfectIsClientInfected(client)) + { + return false; + } + + // Block damage if there's an infection in progress. + + } + } + + PrintToChatAll("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. + * + * @return True if damage was blocked, false otherwise. + */ +bool:ImmunityOnClientDamage(client, attacker, &Float:damage) +{ + PrintToChatAll("ImmunityOnClientDamage(client=%d, attacker=%d, damage=%f)", client, attacker, damage); + + // 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); + + switch(mode) + { + 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)) + { + PrintToChatAll("ImmunityOnClientDamage - Below threshold, removing damage."); + // Client is about to be infected. Remove damage so the client + // won't die. + //damage = 0.0; + + // 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 true; + } + } + } + + // Allow damage. + return false; } /** @@ -111,15 +290,53 @@ bool:ImmunityOnClientKnockBack(client) } /** - * Handles damage mode immunity. + * 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:ImmunityDamageModeHandler(client) +bool:ImmunityInfectModeHandler(client, attacker) { + PrintToChatAll("ImmunityInfectModeHandler(client=%d, attacker=%d)", client, attacker); + + // 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); + PrintToChatAll("threshold=%d", threshold); + + // Check if infection is disabled. + if (threshold == 0) + { + // Infection is handled here: blocked. + PrintToChatAll("Infection disabled, block infection."); + return true; + } + + if (PlayerImmunityThresholdPassed[client]) + { + // Client HP below threshold, allow instant infection. + return false; + } + + /*new clientHP = GetClientHealth(client); + PrintToChatAll("clientHP=%d", clientHP); + + if (clientHP < threshold) + { + // Client HP below threshold, allow instant infection. + return false; + }*/ + + PrintToChatAll("Above threshold, block infection."); + return true; } /** @@ -130,55 +347,146 @@ bool:ImmunityDamageModeHandler(client) * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ -bool:ImmunityDelayModeHandler(client) +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. + + // Trigger timer event to reduce duration. + ImmunityDelayTimerHandler(PlayerImmunityTimer[client], client); + + // Block infection. + return false; + } + + // Start a delayed infection. Initialize duration and attacker. + PlayerImmunityDuration[client] = ClassGetImmunityAmount(client); + PlayerImmunityAttacker[client] = attacker; + + // Create repated 1-second timer. + PlayerImmunityTimer[client] = CreateTimer(1.0, ImmunityDelayTimerHandler, client, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT); + + // Block infection. + return false; +} + +public Action:ImmunityDelayTimerHandler(Handle:timer, any:client) +{ + // Verify that client is connected and alive. + if (!IsClientInGame(client) || !IsPlayerAlive(client)) + { + // Client disconnected or died. Abort immunity action. + PlayerImmunityTimer[client] = INVALID_HANDLE; + ImmunityAbortHandler(client, false); + return Plugin_Stop; + } + + // TODO: Read cvar for reduction amount. + new reduction = 2; + + // Reduce duration. + PlayerImmunityDuration[client] -= reduction; + + // Check if time is up. + if (PlayerImmunityDuration[client] <= 0) + { + // Time is up. Reset data. + PlayerImmunityDuration[client] = 0; + PlayerImmunityTimer[client] = INVALID_HANDLE; + ZREndTimer(PlayerImmunityTimer[client]); + + // Infect client. + InfectHumanToZombie(client, PlayerImmunityAttacker[client]); + + return Plugin_Stop; + } + + return Plugin_Continue; } /** * Handles shield mode immunity. * - * @param client Client that's being infected. + * Zombies will get a shield against knock back, while humans become immune of + * infections. + * + * @param client Client deploying shield. + * @param asZombie Client is a zombie. * * @return True if infection will be handled by this module, false if * infection can be applied instantly. */ -bool:ImmunityShieldModeHandler(client) +bool:ImmunityShieldModeHandler(client, bool:asZombie = true) { + return false; } /** - * Aborts any immunity mode in action (shields, delays, etc.). + * Aborts any immunity mode in action (shields, delays, etc.). Resets values. * - * @param client Client that's aborting immunity mode actions. + * @param client Client that's aborting immunity mode actions. + * @param resetLastUse Reset timestamp of last use. This will reset cooldown. */ -ImmunityAbortHandler(client) +ImmunityAbortHandler(client, bool:resetLastUse = true) { - // TODO: Stop timers, disable shield. + // Stop timer, if running. + ZREndTimer(PlayerImmunityTimer[client]); + + // Reset data. + PlayerImmunityDuration[client] = -1; + PlayerImmunityAttacker[client] = 0; + PlayerImmunityThresholdPassed[client] = false; + + if (resetLastUse) + { + PlayerImmunityLastUse[client] = 0; + } } -ImmunityAbortAll() +/** + * 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); + } } ImmunityOnClientHuman(client) { + ImmunityAbortHandler(client); } -ImmunityOnClientDeath(client, attacker) +ImmunityOnClientDeath(client) { + ImmunityAbortHandler(client, false); } ImmunityClientInit(client) { - // Abord old actions, initialize variables. + ImmunityAbortHandler(client); } -ImmunityOnClientDisconnect(client, attacker) +ImmunityClientSpawn(client) { + ImmunityAbortHandler(client, false); } -ImmunityOnClientTeam(client, team) +ImmunityOnClientDisconnect(client) { + // Loop through attacker cache and remove client (set to 0). +} + +ImmunityOnClientTeam(client) +{ + ImmunityAbortHandler(client); } ImmunityOnRoundEnd() @@ -191,6 +499,23 @@ ImmunityOnMapEnd() ImmunityAbortAll(); } +bool:ImmunityBelowInfectThreshold(client, Float:damage) +{ + new threshold = ClassGetImmunityAmount(client); + new clientHP = GetClientHealth(client); + new dmg = RoundToNearest(damage); + + PrintToChatAll("threshold=%d, clientHp=%d", threshold, clientHP); + + // Check if the damage go below the HP threshold. + if (clientHP - dmg <= 0.0 && threshold > 0) + { + return true; + } + + return false; +} + /** * Converts a string to an immunity mode. * diff --git a/src/zr/infect.inc b/src/zr/infect.inc index 80e2d69..708af00 100644 --- a/src/zr/infect.inc +++ b/src/zr/infect.inc @@ -301,7 +301,8 @@ InfectOnClientDeath(client, attacker) ToolsClientScore(attacker, true, true, score + bonus); } -/** Client has been hurt. +/** + * Client has been hurt. * * @param client The client index. * @param attacker The attacker index. @@ -339,6 +340,13 @@ InfectOnClientHurt(client, attacker, const String:weapon[]) return; } + // Check if the immunity module is handling the infection. + if (ImmunityOnClientInfect(client, attacker)) + { + PrintToChatAll("InfectOnClientHurt - Infect blocked."); + return; + } + // Infect client. InfectHumanToZombie(client, attacker); }