Couch changes
This commit is contained in:
+550
-31
@@ -8,6 +8,11 @@
|
||||
#define COUCH_CORE
|
||||
#include <Couch>
|
||||
|
||||
// Optional: without Zombie:Reloaded nobody counts as a zombie, so couch HP never changes.
|
||||
#undef REQUIRE_PLUGIN
|
||||
#include <zombiereloaded>
|
||||
#define REQUIRE_PLUGIN
|
||||
|
||||
#pragma semicolon 1
|
||||
#pragma newdecls required
|
||||
|
||||
@@ -28,8 +33,8 @@
|
||||
#define COUCH_TEXT_HOLD "8.0"
|
||||
#define COUCH_TEXT_COLOUR "255 144 32"
|
||||
|
||||
// Beacon timer interval, ticks per colour, ticks per light pulse.
|
||||
#define COUCH_BEACON_INTERVAL 0.1
|
||||
// Effect timer interval, ticks per colour, ticks per light pulse.
|
||||
#define COUCH_EFFECT_INTERVAL 0.1
|
||||
#define COUCH_COLOUR_TICKS 4
|
||||
#define COUCH_PULSE_TICKS 10
|
||||
|
||||
@@ -38,6 +43,17 @@
|
||||
#define COUCH_LIGHT_RADIUS_MIN 150.0
|
||||
#define COUCH_LIGHT_RADIUS_MAX 320.0
|
||||
|
||||
// Beacon, after sm_beacon: two beam rings a second from just above the couch's origin. No sound.
|
||||
#define COUCH_BEACON_INTERVAL 1.0
|
||||
#define COUCH_BEACON_HEIGHT 10.0
|
||||
#define COUCH_BEACON_SPRITE_BEAM "sprites/laser.vmt"
|
||||
#define COUCH_BEACON_SPRITE_HALO "sprites/halo01.vmt"
|
||||
|
||||
// Outer ring is sm_beacon's grey; the inner ring is the couch's own orange, or the effect's
|
||||
// current colour while the effect is on.
|
||||
static const int g_iBeaconOuterColour[4] = { 128, 128, 128, 255 };
|
||||
static const int g_iBeaconInnerColour[4] = { 255, 144, 32, 255 };
|
||||
|
||||
static const int g_iCouchColours[][3] =
|
||||
{
|
||||
{ 255, 48, 48 }, // red
|
||||
@@ -49,6 +65,39 @@ static const int g_iCouchColours[][3] =
|
||||
{ 255, 80, 224 }, // magenta
|
||||
};
|
||||
|
||||
// Couch HP: regen is checked this often while the couch is below full.
|
||||
#define COUCH_REGEN_INTERVAL 1.0
|
||||
|
||||
// Stock sounds, so they only need precaching. A zombie hit plays one of the first set at the couch;
|
||||
// the break plays one of each of the other two, the explosion for everyone regardless of distance.
|
||||
static const char g_sHitSounds[][] =
|
||||
{
|
||||
"physics/wood/wood_furniture_break1.wav",
|
||||
"physics/wood/wood_furniture_break2.wav",
|
||||
"physics/wood/wood_box_break1.wav",
|
||||
"physics/wood/wood_box_break2.wav",
|
||||
"physics/wood/wood_plank_break1.wav",
|
||||
"physics/wood/wood_plank_break2.wav",
|
||||
"physics/wood/wood_plank_break3.wav",
|
||||
"physics/wood/wood_plank_break4.wav",
|
||||
};
|
||||
|
||||
static const char g_sBreakSounds[][] =
|
||||
{
|
||||
"physics/wood/wood_crate_break1.wav",
|
||||
"physics/wood/wood_crate_break2.wav",
|
||||
"physics/wood/wood_crate_break3.wav",
|
||||
"physics/wood/wood_crate_break4.wav",
|
||||
"physics/wood/wood_crate_break5.wav",
|
||||
};
|
||||
|
||||
static const char g_sExplodeSounds[][] =
|
||||
{
|
||||
"weapons/hegrenade/explode3.wav",
|
||||
"weapons/hegrenade/explode4.wav",
|
||||
"weapons/hegrenade/explode5.wav",
|
||||
};
|
||||
|
||||
// Allowed to move the couch in pistol-only mode, along with the knife.
|
||||
static const char g_sPistols[][] =
|
||||
{
|
||||
@@ -66,6 +115,8 @@ ConVar g_cvPistolOnly;
|
||||
ConVar g_cvAllowUse;
|
||||
ConVar g_cvEffect;
|
||||
ConVar g_cvEffectIntensity;
|
||||
ConVar g_cvBeacon;
|
||||
ConVar g_cvBeaconRadius;
|
||||
ConVar g_cvIgnoreClip;
|
||||
ConVar g_cvCollisionGroup;
|
||||
|
||||
@@ -74,11 +125,26 @@ ConVar g_cvCollisionGroup;
|
||||
ConVar g_cvMoveTier;
|
||||
ConVar g_cvButtonTier;
|
||||
|
||||
// Couch HP. Only zombie hits lower it; it is kept here rather than in the entity's health so the
|
||||
// engine never breaks the prop itself.
|
||||
ConVar g_cvHp;
|
||||
ConVar g_cvHpMax;
|
||||
ConVar g_cvHpRegenDelay;
|
||||
ConVar g_cvHpRegen;
|
||||
|
||||
GlobalForward g_fwdOnCouchSpawned;
|
||||
GlobalForward g_fwdOnCouchRemoved;
|
||||
GlobalForward g_fwdOnCouchDamaged;
|
||||
GlobalForward g_fwdOnCouchInteraction;
|
||||
GlobalForward g_fwdOnClientBlocked;
|
||||
GlobalForward g_fwdOnCouchHpChanged;
|
||||
GlobalForward g_fwdOnCouchBroken;
|
||||
|
||||
int g_iHp;
|
||||
float g_fLastZombieHit; // GetGameTime() of the last hit that lowered the HP; regen waits on it.
|
||||
Handle g_hRegenTimer;
|
||||
bool g_bBroken; // Set by the breaking hit so the rest of its pellets, and anything else until the next spawn, is ignored.
|
||||
bool g_bZRLoaded;
|
||||
|
||||
int g_iCouchRef = INVALID_ENT_REFERENCE;
|
||||
int g_iLightRef = INVALID_ENT_REFERENCE;
|
||||
@@ -88,8 +154,19 @@ bool g_bPlaced;
|
||||
float g_vecPlaceOrigin[3];
|
||||
float g_vecPlaceAngles[3];
|
||||
|
||||
Handle g_hEffectTimer;
|
||||
int g_iEffectTick;
|
||||
|
||||
// Colour the effect last painted the couch; the beacon's inner ring follows it.
|
||||
int g_iEffectColour[4] = { 255, 255, 255, 255 };
|
||||
|
||||
Handle g_hBeaconTimer;
|
||||
int g_iBeaconTick;
|
||||
int g_iBeaconBeamSprite = -1;
|
||||
int g_iBeaconHaloSprite = -1;
|
||||
|
||||
// Accepted hits this round, announced at round end. Shotguns count once per pellet.
|
||||
int g_iRoundHits;
|
||||
float g_fRoundDamage;
|
||||
|
||||
Handle g_hForceVPhysicsCollide;
|
||||
|
||||
@@ -127,12 +204,20 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
|
||||
CreateNative("Couch_Remove", Native_Remove);
|
||||
CreateNative("Couch_IsEffectEnabled", Native_IsEffectEnabled);
|
||||
CreateNative("Couch_SetEffectEnabled", Native_SetEffectEnabled);
|
||||
CreateNative("Couch_IsBeaconEnabled", Native_IsBeaconEnabled);
|
||||
CreateNative("Couch_SetBeaconEnabled", Native_SetBeaconEnabled);
|
||||
CreateNative("Couch_IsPistolOnly", Native_IsPistolOnly);
|
||||
CreateNative("Couch_SetPistolOnly", Native_SetPistolOnly);
|
||||
CreateNative("Couch_IsUseAllowed", Native_IsUseAllowed);
|
||||
CreateNative("Couch_SetUseAllowed", Native_SetUseAllowed);
|
||||
CreateNative("Couch_SetClientBlocked", Native_SetClientBlocked);
|
||||
CreateNative("Couch_IsClientBlocked", Native_IsClientBlocked);
|
||||
CreateNative("Couch_IsHpEnabled", Native_IsHpEnabled);
|
||||
CreateNative("Couch_SetHpEnabled", Native_SetHpEnabled);
|
||||
CreateNative("Couch_GetHp", Native_GetHp);
|
||||
CreateNative("Couch_SetHp", Native_SetHp);
|
||||
CreateNative("Couch_GetMaxHp", Native_GetMaxHp);
|
||||
CreateNative("Couch_SetMaxHp", Native_SetMaxHp);
|
||||
RegPluginLibrary("Couch");
|
||||
|
||||
MarkNativeAsOptional("GetPlayerTier_native");
|
||||
@@ -147,6 +232,8 @@ public void OnPluginStart()
|
||||
g_fwdOnCouchDamaged = new GlobalForward("Couch_OnCouchDamaged", ET_Ignore, Param_Cell, Param_Cell, Param_Float);
|
||||
g_fwdOnCouchInteraction = new GlobalForward("Couch_OnCouchInteraction", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String);
|
||||
g_fwdOnClientBlocked = new GlobalForward("Couch_OnClientBlocked", ET_Ignore, Param_Cell, Param_Cell);
|
||||
g_fwdOnCouchHpChanged = new GlobalForward("Couch_OnCouchHpChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
|
||||
g_fwdOnCouchBroken = new GlobalForward("Couch_OnCouchBroken", ET_Ignore, Param_Cell, Param_Cell);
|
||||
|
||||
g_cvModel = CreateConVar("sm_couch_model", COUCH_DEFAULT_MODEL, "Model used for the couch. Applies to the next spawn.", FCVAR_NOTIFY);
|
||||
g_cvPushScale = CreateConVar("sm_couch_push_scale", "6.0", "Multiplier on the force bullets apply to the couch. 1.0 is the engine's own push.", FCVAR_NOTIFY, true, 0.0, true, 100.0);
|
||||
@@ -154,18 +241,28 @@ public void OnPluginStart()
|
||||
g_cvAllowUse = CreateConVar("sm_allow_couch_use", "1", "Players may push the couch by holding +use on it. 0 means it can only be moved by shooting it.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
||||
g_cvEffect = CreateConVar("sm_couch_effect", "1", "Colour cycle and pulsing light on the couch.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
||||
g_cvEffectIntensity = CreateConVar("sm_couch_effect_intensity", "2", "Brightness of the couch effect light. 0 is off, 2 is the default, 8 is very bright. Applies immediately.", FCVAR_NOTIFY, true, 0.0, true, 10.0);
|
||||
g_cvBeacon = CreateConVar("sm_couch_beacon", "0", "Beacon on the couch, like sm_beacon: expanding rings every second.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
||||
g_cvBeaconRadius = CreateConVar("sm_couch_beacon_radius", "375", "Radius of the couch beacon's rings.", FCVAR_NOTIFY, true, 50.0, true, 1500.0);
|
||||
g_cvIgnoreClip = CreateConVar("sm_couch_ignore_clip", "1", "Let the couch pass through func_clip_vphysics brushes.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
||||
g_cvMoveTier = CreateConVar("sm_couch_move_tier", "0", "Minimum playtime tier needed to shoot or +use the couch.", FCVAR_NOTIFY, true, 0.0);
|
||||
g_cvButtonTier = CreateConVar("sm_couch_button_tier", "0", "Minimum playtime tier needed to press or shoot map buttons (func_button).", FCVAR_NOTIFY, true, 0.0);
|
||||
g_cvCollisionGroup = CreateConVar("sm_couch_collision_group", "5", "Collision group given to the couch at spawn. 5 (player), 0 (prop)", FCVAR_NOTIFY, true, 0.0, true, 20.0);
|
||||
g_cvHp = CreateConVar("sm_couch_hp", "1", "Couch HP: zombie hits wear the couch down and at 0 HP it breaks, killing every human.", FCVAR_NOTIFY, true, 0.0, true, 1.0);
|
||||
g_cvHpMax = CreateConVar("sm_couch_hp_max", "4000", "Maximum couch HP. The couch spawns with this much every round.", FCVAR_NOTIFY, true, 1.0);
|
||||
g_cvHpRegenDelay = CreateConVar("sm_couch_hp_regen_delay", "10","Seconds without a zombie hit before the couch starts regenerating HP.", FCVAR_NOTIFY, true, 0.0);
|
||||
g_cvHpRegen = CreateConVar("sm_couch_hp_regen", "25", "HP the couch regenerates per second once the regen delay has passed.", FCVAR_NOTIFY, true, 0.0);
|
||||
|
||||
g_cvModel.AddChangeHook(OnModelChanged);
|
||||
g_cvEffect.AddChangeHook(OnEffectChanged);
|
||||
g_cvEffectIntensity.AddChangeHook(OnEffectIntensityChanged);
|
||||
g_cvBeacon.AddChangeHook(OnBeaconChanged);
|
||||
g_cvHp.AddChangeHook(OnHpEnabledChanged);
|
||||
g_cvHpMax.AddChangeHook(OnHpMaxChanged);
|
||||
|
||||
AutoExecConfig(true, "Couch");
|
||||
|
||||
HookEvent("round_start", Event_RoundStart, EventHookMode_Post);
|
||||
HookEvent("round_end", Event_RoundEnd, EventHookMode_Post);
|
||||
|
||||
g_cvTurboPhysics = FindConVar("sv_turbophysics");
|
||||
|
||||
@@ -196,16 +293,42 @@ public void OnPluginEnd()
|
||||
KillCouch();
|
||||
}
|
||||
|
||||
public void OnAllPluginsLoaded()
|
||||
{
|
||||
g_bZRLoaded = LibraryExists("zombiereloaded");
|
||||
}
|
||||
|
||||
public void OnLibraryAdded(const char[] name)
|
||||
{
|
||||
if (StrEqual(name, "zombiereloaded"))
|
||||
g_bZRLoaded = true;
|
||||
}
|
||||
|
||||
public void OnLibraryRemoved(const char[] name)
|
||||
{
|
||||
if (StrEqual(name, "zombiereloaded"))
|
||||
g_bZRLoaded = false;
|
||||
}
|
||||
|
||||
public void OnMapStart()
|
||||
{
|
||||
PrecacheCouchModel();
|
||||
PrecacheBeacon();
|
||||
PrecacheHpSounds();
|
||||
|
||||
// Entities and no-mapchange timers went with the map.
|
||||
g_iCouchRef = INVALID_ENT_REFERENCE;
|
||||
g_iLightRef = INVALID_ENT_REFERENCE;
|
||||
g_bPlaced = false;
|
||||
g_hEffectTimer = null;
|
||||
g_iEffectTick = 0;
|
||||
g_hBeaconTimer = null;
|
||||
g_iBeaconTick = 0;
|
||||
g_iRoundHits = 0;
|
||||
g_fRoundDamage = 0.0;
|
||||
g_hRegenTimer = null;
|
||||
g_iHp = 0;
|
||||
g_fLastZombieHit = 0.0;
|
||||
g_bBroken = false;
|
||||
|
||||
ResetBlockedClients();
|
||||
}
|
||||
@@ -228,7 +351,9 @@ public void OnEntityDestroyed(int entity)
|
||||
// until the next round start.
|
||||
g_iCouchRef = INVALID_ENT_REFERENCE;
|
||||
KillLight();
|
||||
StopEffect();
|
||||
StopBeacon();
|
||||
StopRegen();
|
||||
|
||||
Call_StartForward(g_fwdOnCouchRemoved);
|
||||
Call_Finish();
|
||||
@@ -250,6 +375,16 @@ public void OnClientDisconnect(int client)
|
||||
|
||||
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
|
||||
{
|
||||
g_iRoundHits = 0;
|
||||
g_fRoundDamage = 0.0;
|
||||
|
||||
// A couch that survived the restart starts the round at full HP too.
|
||||
if (GetCouchIndex() != -1)
|
||||
{
|
||||
ResetHp();
|
||||
UpdateRegen();
|
||||
}
|
||||
|
||||
if (!g_bPlaced || GetCouchIndex() != -1)
|
||||
return;
|
||||
|
||||
@@ -260,13 +395,33 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
|
||||
CreateTimer(COUCH_ANNOUNCE_DELAY, Timer_AnnounceCouchRound, _, TIMER_FLAG_NO_MAPCHANGE);
|
||||
}
|
||||
|
||||
// Only for rounds that had a couch: placed, or spawned mid-round and shot.
|
||||
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
|
||||
{
|
||||
if (!g_bPlaced && g_iRoundHits == 0)
|
||||
return;
|
||||
|
||||
if (g_iRoundHits == 0)
|
||||
{
|
||||
PrintToChatAll(PREFIX ... "\x01The couch took \x04no damage\x01 this round.");
|
||||
return;
|
||||
}
|
||||
|
||||
char sDamage[16];
|
||||
char sHits[16];
|
||||
FormatThousands(RoundToNearest(g_fRoundDamage), sDamage, sizeof(sDamage));
|
||||
FormatThousands(g_iRoundHits, sHits, sizeof(sHits));
|
||||
|
||||
PrintToChatAll(PREFIX ... "\x01The couch took \x04%s damage\x01 from \x04%s hit%s\x01 this round.", sDamage, sHits, (g_iRoundHits == 1) ? "" : "s");
|
||||
}
|
||||
|
||||
public Action Timer_AnnounceCouchRound(Handle timer)
|
||||
{
|
||||
if (GetCouchIndex() == -1)
|
||||
return Plugin_Stop;
|
||||
|
||||
ShowCouchRoundText();
|
||||
PrintToChatAll("\x01[SM] \x04Couch round!\x01 Humans must bring the couch with them throughout the map or they fail the round.");
|
||||
PrintToChatAll(PREFIX ... "Couch round!\x01 Humans must bring the couch with them throughout the map or they fail the round.");
|
||||
|
||||
return Plugin_Stop;
|
||||
}
|
||||
@@ -450,12 +605,14 @@ int SpawnCouchAt(const float vecOrigin[3], const float vecAngles[3])
|
||||
// SDKHooks drops the hooks with the entity.
|
||||
SDKHook(couch, SDKHook_OnTakeDamage, OnCouchTakeDamage);
|
||||
|
||||
// Takes the beacon's colour modulation at full opacity.
|
||||
// Takes the effect's colour modulation at full opacity.
|
||||
SetEntityRenderMode(couch, RENDER_NORMAL);
|
||||
|
||||
g_iCouchRef = EntIndexToEntRef(couch);
|
||||
|
||||
ResetHp();
|
||||
ApplyEffectState();
|
||||
ApplyBeaconState();
|
||||
|
||||
Call_StartForward(g_fwdOnCouchSpawned);
|
||||
Call_PushCell(couch);
|
||||
@@ -471,7 +628,9 @@ bool KillCouch()
|
||||
|
||||
// Cleared first so OnEntityDestroyed doesn't run the external-removal path too.
|
||||
g_iCouchRef = INVALID_ENT_REFERENCE;
|
||||
StopEffect();
|
||||
StopBeacon();
|
||||
StopRegen();
|
||||
KillLight();
|
||||
|
||||
if (couch == -1)
|
||||
@@ -494,11 +653,33 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
|
||||
{
|
||||
bool bFromClient = (attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker));
|
||||
|
||||
if (bFromClient && IsClientRefused(victim, attacker))
|
||||
return Plugin_Handled;
|
||||
// With couch HP on, a zombie's hit always costs HP: couch bans, the tier and pistol-only mode don't
|
||||
// stop it. A banned zombie's hit still counts and is reported, but is refused at the end so it
|
||||
// doesn't move the couch.
|
||||
bool bRefusePush = false;
|
||||
|
||||
if (bFromClient && g_cvPistolOnly.BoolValue && !IsPistolOrKnifeHit(attacker, weapon, damagetype))
|
||||
return Plugin_Handled;
|
||||
if (bFromClient && g_cvHp.BoolValue && IsZombie(attacker))
|
||||
{
|
||||
if (g_bBroken)
|
||||
return Plugin_Handled;
|
||||
|
||||
if (ApplyZombieHit(victim, attacker, damage))
|
||||
return Plugin_Handled;
|
||||
|
||||
bRefusePush = g_bBlocked[attacker];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bFromClient && IsClientRefused(victim, attacker))
|
||||
return Plugin_Handled;
|
||||
|
||||
if (bFromClient && g_cvPistolOnly.BoolValue && !IsPistolOrKnifeHit(attacker, weapon, damagetype))
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
// Counted only once the hit is accepted, so refused shots don't show in the round total.
|
||||
g_iRoundHits++;
|
||||
g_fRoundDamage += damage;
|
||||
|
||||
Action result = Plugin_Continue;
|
||||
|
||||
@@ -520,7 +701,210 @@ public Action OnCouchTakeDamage(int victim, int &attacker, int &inflictor, float
|
||||
ReportHit(victim, attacker, inflictor, weapon, damagetype);
|
||||
}
|
||||
|
||||
return result;
|
||||
return bRefusePush ? Plugin_Handled : result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Couch HP
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool IsZombie(int client)
|
||||
{
|
||||
return g_bZRLoaded && ZR_IsClientZombie(client);
|
||||
}
|
||||
|
||||
void PrecacheHpSounds()
|
||||
{
|
||||
for (int i = 0; i < sizeof(g_sHitSounds); i++)
|
||||
PrecacheSound(g_sHitSounds[i], true);
|
||||
|
||||
for (int i = 0; i < sizeof(g_sBreakSounds); i++)
|
||||
PrecacheSound(g_sBreakSounds[i], true);
|
||||
|
||||
for (int i = 0; i < sizeof(g_sExplodeSounds); i++)
|
||||
PrecacheSound(g_sExplodeSounds[i], true);
|
||||
}
|
||||
|
||||
public void OnHpEnabledChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||
{
|
||||
if (convar.BoolValue)
|
||||
{
|
||||
ResetHp();
|
||||
UpdateRegen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Announced so the HUD drops its HP line.
|
||||
StopRegen();
|
||||
FireHpChanged();
|
||||
}
|
||||
|
||||
// A full couch stays full; anything else is clamped to the new maximum.
|
||||
public void OnHpMaxChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||
{
|
||||
int iOldMax = StringToInt(oldValue);
|
||||
int iNewMax = convar.IntValue;
|
||||
|
||||
if (g_iHp >= iOldMax || g_iHp > iNewMax)
|
||||
g_iHp = iNewMax;
|
||||
|
||||
FireHpChanged();
|
||||
UpdateRegen();
|
||||
}
|
||||
|
||||
// Full HP; on spawn, round start and when the feature is switched on.
|
||||
void ResetHp()
|
||||
{
|
||||
g_iHp = g_cvHpMax.IntValue;
|
||||
g_bBroken = false;
|
||||
g_fLastZombieHit = 0.0;
|
||||
FireHpChanged();
|
||||
}
|
||||
|
||||
// Clamped to 0..max. Doesn't touch the regen timer; callers decide that.
|
||||
void SetHp(int hp)
|
||||
{
|
||||
int iMax = g_cvHpMax.IntValue;
|
||||
if (hp < 0)
|
||||
hp = 0;
|
||||
else if (hp > iMax)
|
||||
hp = iMax;
|
||||
|
||||
if (hp == g_iHp)
|
||||
return;
|
||||
|
||||
g_iHp = hp;
|
||||
FireHpChanged();
|
||||
}
|
||||
|
||||
void FireHpChanged()
|
||||
{
|
||||
Call_StartForward(g_fwdOnCouchHpChanged);
|
||||
Call_PushCell(GetCouchIndex());
|
||||
Call_PushCell(g_iHp);
|
||||
Call_PushCell(g_cvHpMax.IntValue);
|
||||
Call_Finish();
|
||||
}
|
||||
|
||||
// Brings the regen timer in line with the state: it runs only while a live couch is below full HP.
|
||||
// Not called from inside Timer_Regen; that returns Plugin_Stop instead.
|
||||
void UpdateRegen()
|
||||
{
|
||||
if (!g_cvHp.BoolValue || g_bBroken || GetCouchIndex() == -1 || g_iHp >= g_cvHpMax.IntValue)
|
||||
{
|
||||
StopRegen();
|
||||
return;
|
||||
}
|
||||
|
||||
StartRegen();
|
||||
}
|
||||
|
||||
void StartRegen()
|
||||
{
|
||||
if (g_hRegenTimer == null)
|
||||
g_hRegenTimer = CreateTimer(COUCH_REGEN_INTERVAL, Timer_Regen, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
|
||||
}
|
||||
|
||||
void StopRegen()
|
||||
{
|
||||
delete g_hRegenTimer;
|
||||
}
|
||||
|
||||
public Action Timer_Regen(Handle timer)
|
||||
{
|
||||
int iMax = g_cvHpMax.IntValue;
|
||||
|
||||
if (!g_cvHp.BoolValue || g_bBroken || GetCouchIndex() == -1 || g_iHp >= iMax)
|
||||
{
|
||||
g_hRegenTimer = null;
|
||||
return Plugin_Stop;
|
||||
}
|
||||
|
||||
// Waiting out the delay since the last hit; a new hit pushes it back.
|
||||
if (GetGameTime() - g_fLastZombieHit < g_cvHpRegenDelay.FloatValue)
|
||||
return Plugin_Continue;
|
||||
|
||||
SetHp(g_iHp + g_cvHpRegen.IntValue);
|
||||
|
||||
if (g_iHp >= iMax)
|
||||
{
|
||||
g_hRegenTimer = null;
|
||||
return Plugin_Stop;
|
||||
}
|
||||
|
||||
return Plugin_Continue;
|
||||
}
|
||||
|
||||
// Takes the hit off the couch's HP and plays a break sound at it. Returns true if this hit broke the couch.
|
||||
bool ApplyZombieHit(int couch, int attacker, float damage)
|
||||
{
|
||||
// Every hit costs at least 1 HP, whatever the knife does to props.
|
||||
int iDamage = RoundToNearest(damage);
|
||||
if (iDamage < 1)
|
||||
iDamage = 1;
|
||||
|
||||
g_fLastZombieHit = GetGameTime();
|
||||
SetHp(g_iHp - iDamage);
|
||||
|
||||
EmitSoundToAll(g_sHitSounds[GetRandomInt(0, sizeof(g_sHitSounds) - 1)], couch, SNDCHAN_STATIC, SNDLEVEL_NORMAL);
|
||||
|
||||
if (g_iHp > 0)
|
||||
{
|
||||
UpdateRegen();
|
||||
return false;
|
||||
}
|
||||
|
||||
BreakCouch(couch, attacker);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 0 HP: the couch breaks loudly, every human dies and the couch is removed; it comes back at full HP
|
||||
// at the next round start if it was placed. attacker is 0 for an admin set.
|
||||
void BreakCouch(int couch, int attacker)
|
||||
{
|
||||
g_bBroken = true;
|
||||
StopRegen();
|
||||
|
||||
// The explosion is heard by everyone at full volume; the splintering is placed at the couch.
|
||||
EmitSoundToAll(g_sExplodeSounds[GetRandomInt(0, sizeof(g_sExplodeSounds) - 1)], SOUND_FROM_PLAYER, SNDCHAN_STATIC, SNDLEVEL_NONE);
|
||||
EmitSoundToAll(g_sBreakSounds[GetRandomInt(0, sizeof(g_sBreakSounds) - 1)], couch, SNDCHAN_STATIC, SNDLEVEL_SCREAMING);
|
||||
|
||||
if (attacker >= 1 && attacker <= MaxClients && IsClientInGame(attacker))
|
||||
PrintToChatAll(PREFIX ... "%N\x01 broke the couch! Game over!", attacker);
|
||||
else
|
||||
PrintToChatAll(PREFIX ... "The couch\x01 broke! Game over!");
|
||||
|
||||
// While the entity still exists, so listeners can read its position.
|
||||
Call_StartForward(g_fwdOnCouchBroken);
|
||||
Call_PushCell(couch);
|
||||
Call_PushCell(attacker);
|
||||
Call_Finish();
|
||||
|
||||
if (g_bZRLoaded)
|
||||
{
|
||||
for (int i = 1; i <= MaxClients; i++)
|
||||
{
|
||||
if (IsClientInGame(i) && IsPlayerAlive(i) && ZR_IsClientHuman(i))
|
||||
KillHuman(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Last: fires Couch_OnCouchRemoved. The Kill input is deferred, so this is safe from inside the
|
||||
// couch's own damage hook; g_bBroken swallows the rest of that hit's pellets.
|
||||
KillCouch();
|
||||
}
|
||||
|
||||
// Kills a human when the couch breaks. Lethal world damage is an ordinary death: it runs the damage
|
||||
// path, fires player_death and leaves a ragdoll, where CommitSuicide can be refused or skip the
|
||||
// ragdoll. The damage is well over health plus kevlar so armour cannot save them; suicide is the
|
||||
// fallback if another plugin swallowed the damage.
|
||||
void KillHuman(int client)
|
||||
{
|
||||
float fDamage = float(GetClientHealth(client) + GetClientArmor(client)) * 2.0 + 1000.0;
|
||||
SDKHooks_TakeDamage(client, 0, 0, fDamage, DMG_GENERIC);
|
||||
|
||||
if (IsPlayerAlive(client))
|
||||
ForcePlayerSuicide(client);
|
||||
}
|
||||
|
||||
// Reach of the engine's +use push.
|
||||
@@ -934,14 +1318,14 @@ void ApplyLightIntensity()
|
||||
AcceptEntityInput(light, "brightness");
|
||||
}
|
||||
|
||||
// Brings the light and beacon in line with sm_couch_effect. Safe to call at any time.
|
||||
// Brings the light and colour cycle in line with sm_couch_effect. Safe to call at any time.
|
||||
void ApplyEffectState()
|
||||
{
|
||||
int couch = GetCouchIndex();
|
||||
|
||||
if (couch == -1 || !g_cvEffect.BoolValue)
|
||||
{
|
||||
StopBeacon();
|
||||
StopEffect();
|
||||
KillLight();
|
||||
|
||||
if (couch != -1)
|
||||
@@ -953,7 +1337,7 @@ void ApplyEffectState()
|
||||
if (GetLightIndex() == -1)
|
||||
g_iLightRef = SpawnCouchLight(couch);
|
||||
|
||||
StartBeacon();
|
||||
StartEffect();
|
||||
}
|
||||
|
||||
// light_dynamic parented to the couch. Returns INVALID_ENT_REFERENCE on failure.
|
||||
@@ -1002,6 +1386,84 @@ void KillLight()
|
||||
AcceptEntityInput(light, "Kill");
|
||||
}
|
||||
|
||||
void StartEffect()
|
||||
{
|
||||
if (g_hEffectTimer == null)
|
||||
g_hEffectTimer = CreateTimer(COUCH_EFFECT_INTERVAL, Timer_Effect, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
|
||||
}
|
||||
|
||||
// Not called from inside Timer_Effect; that returns Plugin_Stop instead.
|
||||
void StopEffect()
|
||||
{
|
||||
delete g_hEffectTimer;
|
||||
}
|
||||
|
||||
public Action Timer_Effect(Handle timer)
|
||||
{
|
||||
int couch = GetCouchIndex();
|
||||
if (couch == -1)
|
||||
{
|
||||
g_hEffectTimer = null;
|
||||
return Plugin_Stop;
|
||||
}
|
||||
|
||||
g_iEffectTick++;
|
||||
|
||||
int colour = (g_iEffectTick / COUCH_COLOUR_TICKS) % sizeof(g_iCouchColours);
|
||||
int r = g_iCouchColours[colour][0];
|
||||
int g = g_iCouchColours[colour][1];
|
||||
int b = g_iCouchColours[colour][2];
|
||||
|
||||
SetEntityRenderColor(couch, r, g, b, 255);
|
||||
|
||||
g_iEffectColour[0] = r;
|
||||
g_iEffectColour[1] = g;
|
||||
g_iEffectColour[2] = b;
|
||||
|
||||
int light = GetLightIndex();
|
||||
if (light == -1)
|
||||
return Plugin_Continue;
|
||||
|
||||
SetEntityRenderColor(light, r, g, b, 255);
|
||||
|
||||
// One sine period per pulse cycle, mapped onto the radius range.
|
||||
float fPhase = float(g_iEffectTick % COUCH_PULSE_TICKS) / float(COUCH_PULSE_TICKS);
|
||||
float fPulse = (Sine(fPhase * 2.0 * FLOAT_PI) + 1.0) / 2.0;
|
||||
|
||||
SetVariantFloat(COUCH_LIGHT_RADIUS_MIN + fPulse * (COUCH_LIGHT_RADIUS_MAX - COUCH_LIGHT_RADIUS_MIN));
|
||||
AcceptEntityInput(light, "distance");
|
||||
|
||||
return Plugin_Continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Beacon
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
public void OnBeaconChanged(ConVar convar, const char[] oldValue, const char[] newValue)
|
||||
{
|
||||
ApplyBeaconState();
|
||||
}
|
||||
|
||||
// The sprites are stock CS:S content, the same ones sm_beacon uses.
|
||||
void PrecacheBeacon()
|
||||
{
|
||||
g_iBeaconBeamSprite = PrecacheModel(COUCH_BEACON_SPRITE_BEAM, true);
|
||||
g_iBeaconHaloSprite = PrecacheModel(COUCH_BEACON_SPRITE_HALO, true);
|
||||
}
|
||||
|
||||
// Brings the beacon in line with sm_couch_beacon. Safe to call at any time.
|
||||
void ApplyBeaconState()
|
||||
{
|
||||
if (GetCouchIndex() == -1 || !g_cvBeacon.BoolValue)
|
||||
{
|
||||
StopBeacon();
|
||||
return;
|
||||
}
|
||||
|
||||
StartBeacon();
|
||||
}
|
||||
|
||||
void StartBeacon()
|
||||
{
|
||||
if (g_hBeaconTimer == null)
|
||||
@@ -1014,6 +1476,7 @@ void StopBeacon()
|
||||
delete g_hBeaconTimer;
|
||||
}
|
||||
|
||||
// Same rings as sm_beacon's Timer_Beacon, from the couch instead of a player, without its blip.
|
||||
public Action Timer_Beacon(Handle timer)
|
||||
{
|
||||
int couch = GetCouchIndex();
|
||||
@@ -1023,27 +1486,26 @@ public Action Timer_Beacon(Handle timer)
|
||||
return Plugin_Stop;
|
||||
}
|
||||
|
||||
g_iBeaconTick++;
|
||||
|
||||
int colour = (g_iBeaconTick / COUCH_COLOUR_TICKS) % sizeof(g_iCouchColours);
|
||||
int r = g_iCouchColours[colour][0];
|
||||
int g = g_iCouchColours[colour][1];
|
||||
int b = g_iCouchColours[colour][2];
|
||||
|
||||
SetEntityRenderColor(couch, r, g, b, 255);
|
||||
|
||||
int light = GetLightIndex();
|
||||
if (light == -1)
|
||||
if (g_iBeaconBeamSprite <= 0 || g_iBeaconHaloSprite <= 0)
|
||||
return Plugin_Continue;
|
||||
|
||||
SetEntityRenderColor(light, r, g, b, 255);
|
||||
float vecOrigin[3];
|
||||
GetEntPropVector(couch, Prop_Send, "m_vecOrigin", vecOrigin);
|
||||
vecOrigin[2] += COUCH_BEACON_HEIGHT;
|
||||
|
||||
// One sine period per pulse cycle, mapped onto the radius range.
|
||||
float fPhase = float(g_iBeaconTick % COUCH_PULSE_TICKS) / float(COUCH_PULSE_TICKS);
|
||||
float fPulse = (Sine(fPhase * 2.0 * FLOAT_PI) + 1.0) / 2.0;
|
||||
int iInnerColour[4];
|
||||
if (g_cvEffect.BoolValue)
|
||||
iInnerColour = g_iEffectColour;
|
||||
else
|
||||
iInnerColour = g_iBeaconInnerColour;
|
||||
|
||||
SetVariantFloat(COUCH_LIGHT_RADIUS_MIN + fPulse * (COUCH_LIGHT_RADIUS_MAX - COUCH_LIGHT_RADIUS_MIN));
|
||||
AcceptEntityInput(light, "distance");
|
||||
float fRadius = g_cvBeaconRadius.FloatValue;
|
||||
|
||||
TE_SetupBeamRingPoint(vecOrigin, 10.0, fRadius, g_iBeaconBeamSprite, g_iBeaconHaloSprite, 0, 15, 0.5, 5.0, 0.0, g_iBeaconOuterColour, 10, 0);
|
||||
TE_SendToAll();
|
||||
|
||||
TE_SetupBeamRingPoint(vecOrigin, 10.0, fRadius, g_iBeaconBeamSprite, g_iBeaconHaloSprite, 0, 10, 0.6, 10.0, 0.5, iInnerColour, 10, 0);
|
||||
TE_SendToAll();
|
||||
|
||||
return Plugin_Continue;
|
||||
}
|
||||
@@ -1151,6 +1613,17 @@ public any Native_SetEffectEnabled(Handle plugin, int numParams)
|
||||
return 0;
|
||||
}
|
||||
|
||||
public any Native_IsBeaconEnabled(Handle plugin, int numParams)
|
||||
{
|
||||
return g_cvBeacon.BoolValue;
|
||||
}
|
||||
|
||||
public any Native_SetBeaconEnabled(Handle plugin, int numParams)
|
||||
{
|
||||
g_cvBeacon.SetBool(view_as<bool>(GetNativeCell(1)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public any Native_IsPistolOnly(Handle plugin, int numParams)
|
||||
{
|
||||
return g_cvPistolOnly.BoolValue;
|
||||
@@ -1191,3 +1664,49 @@ public any Native_IsClientBlocked(Handle plugin, int numParams)
|
||||
|
||||
return g_bBlocked[client];
|
||||
}
|
||||
|
||||
public any Native_IsHpEnabled(Handle plugin, int numParams)
|
||||
{
|
||||
return g_cvHp.BoolValue;
|
||||
}
|
||||
|
||||
public any Native_SetHpEnabled(Handle plugin, int numParams)
|
||||
{
|
||||
g_cvHp.SetBool(view_as<bool>(GetNativeCell(1)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public any Native_GetHp(Handle plugin, int numParams)
|
||||
{
|
||||
return (GetCouchIndex() == -1) ? 0 : g_iHp;
|
||||
}
|
||||
|
||||
// 0 breaks the couch like a zombie hit would, which gives admins a way to test the break.
|
||||
public any Native_SetHp(Handle plugin, int numParams)
|
||||
{
|
||||
SetHp(GetNativeCell(1));
|
||||
|
||||
int couch = GetCouchIndex();
|
||||
if (g_iHp == 0 && g_cvHp.BoolValue && !g_bBroken && couch != -1)
|
||||
BreakCouch(couch, 0);
|
||||
else
|
||||
UpdateRegen();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public any Native_GetMaxHp(Handle plugin, int numParams)
|
||||
{
|
||||
return g_cvHpMax.IntValue;
|
||||
}
|
||||
|
||||
// The cvar's change hook keeps a full couch full and clamps the rest.
|
||||
public any Native_SetMaxHp(Handle plugin, int numParams)
|
||||
{
|
||||
int maxhp = GetNativeCell(1);
|
||||
if (maxhp < 1)
|
||||
maxhp = 1;
|
||||
|
||||
g_cvHpMax.SetInt(maxhp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+218
-19
@@ -4,8 +4,6 @@
|
||||
#pragma semicolon 1
|
||||
#pragma newdecls required
|
||||
|
||||
#define PREFIX "\x01[SM] \x04"
|
||||
|
||||
// A player who keeps moving the couch the same way is reported at most once per this many seconds.
|
||||
#define LOG_REPEAT_INTERVAL 2.0
|
||||
|
||||
@@ -15,7 +13,8 @@
|
||||
// So commands fail with a message instead of an unbound-native error while the core is unloaded.
|
||||
bool g_bCoreLoaded;
|
||||
|
||||
// Admins who asked to see couch interactions. Per slot, so it survives map changes.
|
||||
// Who sees couch interactions in their console. On by default for anyone who can run sm_couchlog,
|
||||
// off for everyone else. Per slot, so it survives map changes.
|
||||
bool g_bLogging[MAXPLAYERS + 1];
|
||||
|
||||
// Per player: what was last reported, and when the same thing may be reported again.
|
||||
@@ -39,8 +38,32 @@ public void OnPluginStart()
|
||||
RegAdminCmd("sm_bringcouch", Command_BringCouch, ADMFLAG_CHANGEMAP, "Brings the couch to where you are aiming");
|
||||
RegAdminCmd("sm_gotocouch", Command_GotoCouch, ADMFLAG_CHANGEMAP, "Teleports you to the couch");
|
||||
RegAdminCmd("sm_removecouch", Command_RemoveCouch, ADMFLAG_CHANGEMAP, "Removes the couch");
|
||||
RegAdminCmd("sm_couchbeacon", Command_Beacon, ADMFLAG_CHANGEMAP, "sm_couchbeacon [0|1] - beacon rings on the couch; no argument toggles");
|
||||
RegAdminCmd("sm_couchpistolonly", Command_PistolOnly, ADMFLAG_CHANGEMAP, "sm_couchpistolonly [0|1] - only pistols and knives move the couch; no argument toggles");
|
||||
RegAdminCmd("sm_couchlog", Command_Log, ADMFLAG_CHANGEMAP, "sm_couchlog [0|1] - show who moves the couch and how in your console; no argument toggles");
|
||||
RegAdminCmd("sm_couchhp", Command_Hp, ADMFLAG_CHANGEMAP, "sm_couchhp [hp] - sets the couch's HP; no argument shows it");
|
||||
RegAdminCmd("sm_couchmaxhp", Command_MaxHp, ADMFLAG_CHANGEMAP, "sm_couchmaxhp <hp> - sets the couch's maximum HP");
|
||||
|
||||
// Late load: admins are already in, so give them their default.
|
||||
for (int client = 1; client <= MaxClients; client++)
|
||||
{
|
||||
if (IsClientInGame(client) && !IsFakeClient(client))
|
||||
g_bLogging[client] = IsLoggingAdmin(client);
|
||||
}
|
||||
}
|
||||
|
||||
// Admin access is only known once the admin check has run, so the default is set here rather
|
||||
// than on connect.
|
||||
public void OnClientPostAdminCheck(int client)
|
||||
{
|
||||
if (!IsFakeClient(client))
|
||||
g_bLogging[client] = IsLoggingAdmin(client);
|
||||
}
|
||||
|
||||
// Follows the command's access, so an admin override on sm_couchlog changes who gets the default.
|
||||
bool IsLoggingAdmin(int client)
|
||||
{
|
||||
return CheckCommandAccess(client, "sm_couchlog", ADMFLAG_CHANGEMAP);
|
||||
}
|
||||
|
||||
// Game time restarts with the map.
|
||||
@@ -123,6 +146,19 @@ public Action Command_RemoveCouch(int client, int args)
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_Beacon(int client, int args)
|
||||
{
|
||||
if (!RequireCore(client))
|
||||
return Plugin_Handled;
|
||||
|
||||
bool bEnable = !Couch_IsBeaconEnabled();
|
||||
if (args >= 1)
|
||||
bEnable = (GetCmdArgInt(1) != 0);
|
||||
|
||||
DoSetBeacon(client, bEnable);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_PistolOnly(int client, int args)
|
||||
{
|
||||
if (!RequireCore(client))
|
||||
@@ -136,6 +172,52 @@ public Action Command_PistolOnly(int client, int args)
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_Hp(int client, int args)
|
||||
{
|
||||
if (!RequireCore(client))
|
||||
return Plugin_Handled;
|
||||
|
||||
if (Couch_GetCouch() == -1)
|
||||
{
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch. Use sm_spawncouch first.");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
if (args < 1)
|
||||
{
|
||||
char sHp[16], sMax[16];
|
||||
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
|
||||
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " Couch HP: %s/%s (couch HP is %s).", sHp, sMax, Couch_IsHpEnabled() ? "on" : "off");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
DoSetHp(client, GetCmdArgInt(1));
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_MaxHp(int client, int args)
|
||||
{
|
||||
if (!RequireCore(client))
|
||||
return Plugin_Handled;
|
||||
|
||||
if (args < 1)
|
||||
{
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchmaxhp <hp>");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
int hp = GetCmdArgInt(1);
|
||||
if (hp < 1)
|
||||
{
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " The maximum HP must be at least 1.");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
DoSetMaxHp(client, hp);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_Log(int client, int args)
|
||||
{
|
||||
if (!RequireInGame(client))
|
||||
@@ -154,7 +236,7 @@ bool RequireInGame(int client)
|
||||
if (client >= 1 && IsClientInGame(client))
|
||||
return true;
|
||||
|
||||
ReplyToCommand(client, "[SM] This command can only be used in-game.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " This command can only be used in-game.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -163,7 +245,7 @@ bool RequireCore(int client)
|
||||
if (g_bCoreLoaded)
|
||||
return true;
|
||||
|
||||
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -175,7 +257,7 @@ void DoSpawn(int client)
|
||||
{
|
||||
if (Couch_Spawn(client) == -1)
|
||||
{
|
||||
ReplyToCommand(client, "[SM] Could not create the couch.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " Could not create the couch.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,7 +269,7 @@ void DoBring(int client)
|
||||
{
|
||||
if (!Couch_Bring(client))
|
||||
{
|
||||
ReplyToCommand(client, "[SM] There is no couch to bring. Use sm_spawncouch first.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to bring. Use sm_spawncouch first.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,13 +281,13 @@ void DoGoto(int client)
|
||||
{
|
||||
if (!IsPlayerAlive(client))
|
||||
{
|
||||
ReplyToCommand(client, "[SM] You must be alive to go to the couch.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " You must be alive to go to the couch.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Couch_Goto(client))
|
||||
{
|
||||
ReplyToCommand(client, "[SM] There is no couch to go to. Use sm_spawncouch first.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to go to. Use sm_spawncouch first.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +299,7 @@ void DoRemove(int client)
|
||||
{
|
||||
if (!Couch_Remove())
|
||||
{
|
||||
ReplyToCommand(client, "[SM] There is no couch to remove.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " There is no couch to remove.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,6 +316,14 @@ void DoToggleEffect(int client)
|
||||
LogAction(client, -1, "\"%L\" turned the couch effect %s.", client, bEnable ? "on" : "off");
|
||||
}
|
||||
|
||||
void DoSetBeacon(int client, bool bEnable)
|
||||
{
|
||||
Couch_SetBeaconEnabled(bEnable);
|
||||
|
||||
ShowActivity2(client, PREFIX, "\x01Turned the couch beacon \x04%s\x01.", bEnable ? "on" : "off");
|
||||
LogAction(client, -1, "\"%L\" turned the couch beacon %s.", client, bEnable ? "on" : "off");
|
||||
}
|
||||
|
||||
// Announced to everyone so players know why their rifle stopped moving the couch.
|
||||
void DoSetPistolOnly(int client, bool bEnable)
|
||||
{
|
||||
@@ -257,6 +347,45 @@ void DoToggleUse(int client)
|
||||
LogAction(client, -1, "\"%L\" %s couch use.", client, bAllow ? "allowed" : "disallowed");
|
||||
}
|
||||
|
||||
void DoSetHpEnabled(int client, bool bEnable)
|
||||
{
|
||||
Couch_SetHpEnabled(bEnable);
|
||||
|
||||
if (bEnable)
|
||||
ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04on\x01, zombies can break the couch now.");
|
||||
else
|
||||
ShowActivity2(client, PREFIX, "\x01Turned couch HP \x04off\x01.");
|
||||
|
||||
LogAction(client, -1, "\"%L\" turned couch HP %s.", client, bEnable ? "on" : "off");
|
||||
}
|
||||
|
||||
// Read back afterwards so the announcement shows the clamped value. 0 breaks the couch.
|
||||
void DoSetHp(int client, int hp)
|
||||
{
|
||||
Couch_SetHp(hp);
|
||||
hp = Couch_GetHp();
|
||||
|
||||
char sHp[16], sMax[16];
|
||||
FormatThousands(hp, sHp, sizeof(sHp));
|
||||
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
|
||||
|
||||
ShowActivity2(client, PREFIX, "\x01Set the couch's \x04HP\x01 to \x04%s/%s\x01.", sHp, sMax);
|
||||
LogAction(client, -1, "\"%L\" set the couch's HP to %d.", client, hp);
|
||||
}
|
||||
|
||||
void DoSetMaxHp(int client, int hp)
|
||||
{
|
||||
Couch_SetMaxHp(hp);
|
||||
hp = Couch_GetMaxHp();
|
||||
|
||||
char sHp[16], sMax[16];
|
||||
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
|
||||
FormatThousands(hp, sMax, sizeof(sMax));
|
||||
|
||||
ShowActivity2(client, PREFIX, "\x01Set the couch's \x04maximum HP\x01 to \x04%s\x01, it is now at \x04%s/%s\x01.", sMax, sHp, sMax);
|
||||
LogAction(client, -1, "\"%L\" set the couch's maximum HP to %d.", client, hp);
|
||||
}
|
||||
|
||||
// The tier cvars belong to the core plugin, so they are looked up by name. sName is "move" or "button".
|
||||
void DoChangeTier(int client, const char[] sName, int iDelta)
|
||||
{
|
||||
@@ -308,7 +437,8 @@ void DoSetPushScale(int client, float fScale)
|
||||
LogAction(client, -1, "\"%L\" set the couch push scale to %.2f.", client, fScale);
|
||||
}
|
||||
|
||||
// Only affects what this admin sees, so it is neither announced nor logged.
|
||||
// Only affects what this admin sees, so it is neither announced nor logged. The choice lasts
|
||||
// until they disconnect; on their next connect the log is on again.
|
||||
void DoSetLogging(int client, bool bEnable)
|
||||
{
|
||||
g_bLogging[client] = bEnable;
|
||||
@@ -316,7 +446,7 @@ void DoSetLogging(int client, bool bEnable)
|
||||
if (bEnable)
|
||||
PrintToChat(client, PREFIX ... "\x01Couch log \x04on\x01, see your console for who moves the couch and how.");
|
||||
else
|
||||
PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01.");
|
||||
PrintToChat(client, PREFIX ... "\x01Couch log \x04off\x01 until you reconnect.");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
@@ -328,7 +458,7 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
|
||||
// Grenades are rare and knock the couch far, so everyone is told. One blast is one report, so no
|
||||
// rate limit is needed. The userid is there so an admin can put it straight into sm_couchban.
|
||||
if (method == CouchMethod_Grenade)
|
||||
PrintToChatAll("\x01[Couch] \x04%N\x01 (#%d) hit the couch with a \x04grenade", client, GetClientUserId(client));
|
||||
PrintToChatAll(PREFIX ... "%N\x01 (#%d) hit the couch with a \x04grenade", client, GetClientUserId(client));
|
||||
|
||||
if (!AnyoneLogging())
|
||||
return;
|
||||
@@ -366,7 +496,7 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
|
||||
if (!g_bLogging[i] || !IsClientInGame(i))
|
||||
continue;
|
||||
|
||||
PrintToConsole(i, "[Couch] %N (#%d) %s", client, userid, sHow);
|
||||
PrintToConsole(i, PREFIX_PLAIN ... " %N (#%d) %s", client, userid, sHow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,18 +537,19 @@ void ShowCouchMenu(int client)
|
||||
menu.AddItem("remove", "Remove couch", ITEMDRAW_DISABLED);
|
||||
|
||||
char sItem[64];
|
||||
FormatEx(sItem, sizeof(sItem), "Effect: %s", Couch_IsEffectEnabled() ? "ON" : "OFF");
|
||||
menu.AddItem("effect", sItem);
|
||||
|
||||
FormatEx(sItem, sizeof(sItem), "Pistol only: %s", Couch_IsPistolOnly() ? "ON" : "OFF");
|
||||
menu.AddItem("pistol", sItem);
|
||||
|
||||
FormatEx(sItem, sizeof(sItem), "Allow USE: %s", Couch_IsUseAllowed() ? "ON" : "OFF");
|
||||
menu.AddItem("use", sItem);
|
||||
|
||||
FormatEx(sItem, sizeof(sItem), "Couch HP: %s", Couch_IsHpEnabled() ? "ON" : "OFF");
|
||||
menu.AddItem("hp", sItem);
|
||||
|
||||
FormatEx(sItem, sizeof(sItem), "Couch logs (for me): %s", g_bLogging[client] ? "ON" : "OFF");
|
||||
menu.AddItem("log", sItem);
|
||||
|
||||
menu.AddItem("effects", "Effects");
|
||||
menu.AddItem("tiers", "Tier requirements");
|
||||
menu.AddItem("pushscale", "Push scale");
|
||||
|
||||
@@ -451,14 +582,19 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
|
||||
DoGoto(client);
|
||||
else if (StrEqual(sInfo, "remove"))
|
||||
DoRemove(client);
|
||||
else if (StrEqual(sInfo, "effect"))
|
||||
DoToggleEffect(client);
|
||||
else if (StrEqual(sInfo, "pistol"))
|
||||
DoSetPistolOnly(client, !Couch_IsPistolOnly());
|
||||
else if (StrEqual(sInfo, "use"))
|
||||
DoToggleUse(client);
|
||||
else if (StrEqual(sInfo, "hp"))
|
||||
DoSetHpEnabled(client, !Couch_IsHpEnabled());
|
||||
else if (StrEqual(sInfo, "log"))
|
||||
DoSetLogging(client, !g_bLogging[client]);
|
||||
else if (StrEqual(sInfo, "effects"))
|
||||
{
|
||||
ShowEffectsMenu(client);
|
||||
return 0;
|
||||
}
|
||||
else if (StrEqual(sInfo, "tiers"))
|
||||
{
|
||||
ShowTierMenu(client);
|
||||
@@ -482,6 +618,69 @@ public int MenuHandler_Couch(Menu menu, MenuAction action, int param1, int param
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Rebuilt on every display so the labels always reflect the current state.
|
||||
void ShowEffectsMenu(int client)
|
||||
{
|
||||
Menu menu = new Menu(MenuHandler_Effects);
|
||||
menu.SetTitle("Couch - Effects");
|
||||
|
||||
char sItem[64];
|
||||
FormatEx(sItem, sizeof(sItem), "Effect: %s", Couch_IsEffectEnabled() ? "ON" : "OFF");
|
||||
menu.AddItem("effect", sItem);
|
||||
|
||||
FormatEx(sItem, sizeof(sItem), "Beacon: %s", Couch_IsBeaconEnabled() ? "ON" : "OFF");
|
||||
menu.AddItem("beacon", sItem);
|
||||
|
||||
menu.ExitBackButton = true;
|
||||
menu.ExitButton = true;
|
||||
menu.Display(client, MENU_TIME_FOREVER);
|
||||
}
|
||||
|
||||
public int MenuHandler_Effects(Menu menu, MenuAction action, int param1, int param2)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case MenuAction_Select:
|
||||
{
|
||||
int client = param1;
|
||||
|
||||
if (!g_bCoreLoaded)
|
||||
{
|
||||
RequireCore(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char sInfo[16];
|
||||
menu.GetItem(param2, sInfo, sizeof(sInfo));
|
||||
|
||||
if (StrEqual(sInfo, "effect"))
|
||||
DoToggleEffect(client);
|
||||
else if (StrEqual(sInfo, "beacon"))
|
||||
DoSetBeacon(client, !Couch_IsBeaconEnabled());
|
||||
|
||||
ShowEffectsMenu(client);
|
||||
}
|
||||
|
||||
case MenuAction_Cancel:
|
||||
{
|
||||
if (param2 == MenuCancel_ExitBack)
|
||||
{
|
||||
if (g_bCoreLoaded)
|
||||
ShowCouchMenu(param1);
|
||||
else
|
||||
RequireCore(param1);
|
||||
}
|
||||
}
|
||||
|
||||
case MenuAction_End:
|
||||
{
|
||||
delete menu;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ShowTierMenu(int client)
|
||||
{
|
||||
Menu menu = new Menu(MenuHandler_Tier);
|
||||
|
||||
+74
-33
@@ -4,8 +4,6 @@
|
||||
#pragma semicolon 1
|
||||
#pragma newdecls required
|
||||
|
||||
#define PREFIX "\x01[SM] \x04"
|
||||
|
||||
// So commands fail with a message instead of an unbound-native error while the core is unloaded.
|
||||
bool g_bCoreLoaded;
|
||||
|
||||
@@ -30,13 +28,13 @@ public Plugin myinfo =
|
||||
|
||||
public void OnPluginStart()
|
||||
{
|
||||
LoadTranslations("common.phrases"); // FindTarget's replies.
|
||||
LoadTranslations("common.phrases"); // Target errors and multi-target names ("all players").
|
||||
|
||||
g_alBannedAccounts = new ArrayList();
|
||||
|
||||
RegAdminCmd("sm_couchban", Command_CouchBan, ADMFLAG_BAN, "sm_couchban <#userid|name> - bans a player from the couch until the end of the map");
|
||||
RegAdminCmd("sm_couchban", Command_CouchBan, ADMFLAG_BAN, "sm_couchban <#userid|name|@filter> - bans players from the couch until the end of the map");
|
||||
RegAdminCmd("sm_cban", Command_CouchBan, ADMFLAG_BAN, "Alias of sm_couchban");
|
||||
RegAdminCmd("sm_couchunban", Command_CouchUnban, ADMFLAG_BAN, "sm_couchunban <#userid|name> - lifts a couch ban");
|
||||
RegAdminCmd("sm_couchunban", Command_CouchUnban, ADMFLAG_BAN, "sm_couchunban <#userid|name|@filter> - lifts couch bans");
|
||||
RegAdminCmd("sm_cunban", Command_CouchUnban, ADMFLAG_BAN, "Alias of sm_couchunban");
|
||||
}
|
||||
|
||||
@@ -143,24 +141,11 @@ public Action Command_CouchBan(int client, int args)
|
||||
|
||||
if (args < 1)
|
||||
{
|
||||
ReplyToCommand(client, "[SM] Usage: sm_couchban <#userid|name>");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchban <#userid|name|@filter>");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
char sArg[MAX_TARGET_LENGTH];
|
||||
GetCmdArg(1, sArg, sizeof(sArg));
|
||||
|
||||
int target = FindTarget(client, sArg, true, true);
|
||||
if (target == -1)
|
||||
return Plugin_Handled;
|
||||
|
||||
if (g_bBanned[target])
|
||||
{
|
||||
ReplyToCommand(client, "[SM] %N is already couch-banned.", target);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
DoBan(client, target);
|
||||
ProcessBanCommand(client, true);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
@@ -171,25 +156,83 @@ public Action Command_CouchUnban(int client, int args)
|
||||
|
||||
if (args < 1)
|
||||
{
|
||||
ReplyToCommand(client, "[SM] Usage: sm_couchunban <#userid|name>");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchunban <#userid|name|@filter>");
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
ProcessBanCommand(client, false);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
// Standard SourceMod targeting: a name, #userid or @filter (@all, @t, @zombies ...). Players already in
|
||||
// the wanted state are skipped. One chat line covers the whole command; each ban is still logged.
|
||||
void ProcessBanCommand(int client, bool bBan)
|
||||
{
|
||||
char sArg[MAX_TARGET_LENGTH];
|
||||
GetCmdArg(1, sArg, sizeof(sArg));
|
||||
|
||||
int target = FindTarget(client, sArg, true, true);
|
||||
if (target == -1)
|
||||
return Plugin_Handled;
|
||||
int targets[MAXPLAYERS];
|
||||
char sTargetName[MAX_TARGET_LENGTH];
|
||||
bool bIsML;
|
||||
|
||||
if (!g_bBanned[target])
|
||||
int count = ProcessTargetString(sArg, client, targets, sizeof(targets), COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, sTargetName, sizeof(sTargetName), bIsML);
|
||||
if (count <= 0)
|
||||
{
|
||||
ReplyToCommand(client, "[SM] %N is not couch-banned.", target);
|
||||
return Plugin_Handled;
|
||||
ReplyToTargetError(client, count);
|
||||
return;
|
||||
}
|
||||
|
||||
DoUnban(client, target);
|
||||
return Plugin_Handled;
|
||||
int affected = 0;
|
||||
int last = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int target = targets[i];
|
||||
if (!IsClientInGame(target) || g_bBanned[target] == bBan)
|
||||
continue;
|
||||
|
||||
if (bBan)
|
||||
DoBan(client, target);
|
||||
else
|
||||
DoUnban(client, target);
|
||||
|
||||
affected++;
|
||||
last = target;
|
||||
}
|
||||
|
||||
if (affected == 0)
|
||||
{
|
||||
if (count == 1)
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " %N is %s couch-banned.", targets[0], bBan ? "already" : "not");
|
||||
else
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " No matching players to couch-%s.", bBan ? "ban" : "unban");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Announced to everyone so players know why the couch stopped reacting to someone. A single
|
||||
// player is named even when they were picked out of a filter.
|
||||
if (affected == 1)
|
||||
{
|
||||
if (bBan)
|
||||
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%N\x01 until the end of the map.", last);
|
||||
else
|
||||
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%N\x01.", last);
|
||||
}
|
||||
else if (bIsML)
|
||||
{
|
||||
if (bBan)
|
||||
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%t\x01 until the end of the map.", sTargetName);
|
||||
else
|
||||
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%t\x01.", sTargetName);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bBan)
|
||||
ShowActivity2(client, PREFIX, "\x01couch-banned \x04%s\x01 until the end of the map.", sTargetName);
|
||||
else
|
||||
ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%s\x01.", sTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
bool RequireCore(int client)
|
||||
@@ -197,7 +240,7 @@ bool RequireCore(int client)
|
||||
if (g_bCoreLoaded)
|
||||
return true;
|
||||
|
||||
ReplyToCommand(client, "[SM] The Couch core plugin is not loaded.");
|
||||
ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -205,14 +248,13 @@ bool RequireCore(int client)
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// The announcement is the command's job, so a filter doesn't produce a line per player.
|
||||
void DoBan(int client, int target)
|
||||
{
|
||||
g_bBanned[target] = true;
|
||||
RememberAccount(target);
|
||||
Couch_SetClientBlocked(target, true);
|
||||
|
||||
// Announced to everyone so players know why the couch stopped reacting to someone.
|
||||
PrintToChatAll("\x01[SM] \x04%N\x01 couch-banned \x04%N\x01 until the end of the map.", client, target);
|
||||
LogAction(client, target, "\"%L\" couch-banned \"%L\" until the end of the map.", client, target);
|
||||
}
|
||||
|
||||
@@ -222,7 +264,6 @@ void DoUnban(int client, int target)
|
||||
ForgetAccount(target);
|
||||
Couch_SetClientBlocked(target, false);
|
||||
|
||||
PrintToChatAll("\x01[SM] \x04%N\x01 removed couch-ban from \x04%N\x01.", client, target);
|
||||
LogAction(client, target, "\"%L\" removed couch-ban from \"%L\".", client, target);
|
||||
}
|
||||
|
||||
|
||||
+147
-15
@@ -2,6 +2,11 @@
|
||||
#include <sdktools>
|
||||
#include <Couch>
|
||||
|
||||
// Optional: without Zombie:Reloaded nobody is tagged as a zombie.
|
||||
#undef REQUIRE_PLUGIN
|
||||
#include <zombiereloaded>
|
||||
#define REQUIRE_PLUGIN
|
||||
|
||||
#pragma semicolon 1
|
||||
#pragma newdecls required
|
||||
|
||||
@@ -18,6 +23,24 @@
|
||||
// Put in front of the name of anyone whose grenade hit the couch recently.
|
||||
#define HUD_GRENADE_TAG "<G!> "
|
||||
|
||||
// Put in front of the name of anyone who was a zombie when they last hit or pushed the couch.
|
||||
#define HUD_ZOMBIE_TAG "<Z> "
|
||||
|
||||
// First line while the couch is below full HP: "♥ 500/1000". It stays up on its own, so it is resent
|
||||
// this often; that also catches players who joined after the last send. Held for two refreshes so a
|
||||
// late timer tick can't let it lapse.
|
||||
#define HUD_HP_HEART "♥"
|
||||
#define HUD_HP_REFRESH 5.0
|
||||
#define HUD_HP_HOLD (HUD_HP_REFRESH * 2.0)
|
||||
|
||||
// While the couch is below full and not healing, the heart blinks between "♥" and "♥!" on every
|
||||
// tick of the expiry timer, so each state shows for HUD_EXPIRE_INTERVAL. Healing means a regen
|
||||
// tick raised the HP within this long; regen ticks once a second, so a healing couch never lapses
|
||||
// between ticks. A drop ends the healing state at once, so the blink starts on the hit. (The HUD
|
||||
// fonts only cover the WGL4 set, which has ♥ but no empty heart, hence the "!" suffix.)
|
||||
#define HUD_HP_HEART_ALERT "!"
|
||||
#define HUD_HP_HEAL_HOLD 1.5
|
||||
|
||||
// A user message holds 255 bytes; HudMsg's fixed fields take 34 and the string its terminator.
|
||||
// Overflowing drops the whole message.
|
||||
#define HUD_MSG_BYTES 255
|
||||
@@ -35,11 +58,19 @@ float g_fExpire[MAXPLAYERS + 1]; // GetGameTime() at which this client drops of
|
||||
float g_fLastHit[MAXPLAYERS + 1]; // Ordering key: the most recent shooter is listed first.
|
||||
float g_fGrenadeUntil[MAXPLAYERS + 1]; // GetGameTime() at which this client loses the grenade tag.
|
||||
float g_fNextUseSend[MAXPLAYERS + 1]; // GetGameTime() before which this client's +use doesn't resend.
|
||||
bool g_bZombieHit[MAXPLAYERS + 1]; // This client was a zombie at their last hit or push.
|
||||
|
||||
bool g_bZRLoaded;
|
||||
|
||||
float g_fNextExpiry; // Soonest expiry or tag drop among the names currently listed.
|
||||
float g_fLastExpiry; // Latest expiry among them; the text is held until then.
|
||||
bool g_bFlushQueued; // A send is already scheduled for the next frame.
|
||||
bool g_bHudShown; // Clients currently have text up, so an empty list must clear it.
|
||||
bool g_bHpLine; // The text starts with the HP line, so it must stay up even with nobody listed.
|
||||
float g_fNextHpRefresh; // GetGameTime() at which the HP line is resent.
|
||||
int g_iLastHp; // HP as of the last Couch_OnCouchHpChanged, or -1 if none seen.
|
||||
float g_fHealUntil; // GetGameTime() until which the couch counts as healing; the heart is solid until then.
|
||||
bool g_bHeartAlert; // The "♥!" state is the one currently up.
|
||||
|
||||
char g_sHudText[HUD_TEXT_MAX + 1];
|
||||
|
||||
@@ -65,6 +96,17 @@ public void OnPluginStart()
|
||||
AutoExecConfig(true, "CouchHud");
|
||||
}
|
||||
|
||||
public void OnAllPluginsLoaded()
|
||||
{
|
||||
g_bZRLoaded = LibraryExists("zombiereloaded");
|
||||
}
|
||||
|
||||
public void OnLibraryAdded(const char[] name)
|
||||
{
|
||||
if (StrEqual(name, "zombiereloaded"))
|
||||
g_bZRLoaded = true;
|
||||
}
|
||||
|
||||
public void OnMapStart()
|
||||
{
|
||||
// TIMER_FLAG_NO_MAPCHANGE already killed the timer.
|
||||
@@ -73,6 +115,11 @@ public void OnMapStart()
|
||||
g_bHudShown = false;
|
||||
g_fNextExpiry = 0.0;
|
||||
g_fLastExpiry = 0.0;
|
||||
g_bHpLine = false;
|
||||
g_fNextHpRefresh = 0.0;
|
||||
g_iLastHp = -1;
|
||||
g_fHealUntil = 0.0;
|
||||
g_bHeartAlert = false;
|
||||
|
||||
for (int i = 1; i <= MAXPLAYERS; i++)
|
||||
{
|
||||
@@ -80,6 +127,7 @@ public void OnMapStart()
|
||||
g_fLastHit[i] = 0.0;
|
||||
g_fGrenadeUntil[i] = 0.0;
|
||||
g_fNextUseSend[i] = 0.0;
|
||||
g_bZombieHit[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +137,7 @@ public void OnClientPutInServer(int client)
|
||||
g_fLastHit[client] = 0.0;
|
||||
g_fGrenadeUntil[client] = 0.0;
|
||||
g_fNextUseSend[client] = 0.0;
|
||||
g_bZombieHit[client] = false;
|
||||
}
|
||||
|
||||
public void OnClientDisconnect(int client)
|
||||
@@ -99,6 +148,7 @@ public void OnClientDisconnect(int client)
|
||||
g_fLastHit[client] = 0.0;
|
||||
g_fGrenadeUntil[client] = 0.0;
|
||||
g_fNextUseSend[client] = 0.0;
|
||||
g_bZombieHit[client] = false;
|
||||
|
||||
if (bListed)
|
||||
QueueFlush();
|
||||
@@ -114,6 +164,8 @@ public void OnLibraryRemoved(const char[] name)
|
||||
{
|
||||
if (StrEqual(name, "Couch"))
|
||||
ResetAll();
|
||||
else if (StrEqual(name, "zombiereloaded"))
|
||||
g_bZRLoaded = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
@@ -129,6 +181,7 @@ public void Couch_OnCouchDamaged(int couch, int attacker, float damage)
|
||||
float fNow = GetGameTime();
|
||||
g_fLastHit[attacker] = fNow;
|
||||
g_fExpire[attacker] = MaxFloat(g_fExpire[attacker], fNow + g_cvTime.FloatValue);
|
||||
g_bZombieHit[attacker] = (g_bZRLoaded && ZR_IsClientZombie(attacker));
|
||||
|
||||
QueueFlush();
|
||||
}
|
||||
@@ -142,6 +195,9 @@ public void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
|
||||
|
||||
float fNow = GetGameTime();
|
||||
|
||||
// Every method lands here, so this covers +use and grenades, which never go through damage.
|
||||
g_bZombieHit[client] = (g_bZRLoaded && ZR_IsClientZombie(client));
|
||||
|
||||
if (method == CouchMethod_Use)
|
||||
{
|
||||
g_fLastHit[client] = fNow;
|
||||
@@ -174,6 +230,27 @@ public void Couch_OnCouchRemoved()
|
||||
ResetAll();
|
||||
}
|
||||
|
||||
// Regen ticks land here once a second; the flush decides whether the line is still wanted. A rise
|
||||
// means a regen tick (or an admin raising it) and keeps the heart solid; a drop means a zombie hit
|
||||
// (or an admin lowering it) and starts the blink at once.
|
||||
public void Couch_OnCouchHpChanged(int couch, int hp, int maxhp)
|
||||
{
|
||||
if (!g_cvEnabled.BoolValue)
|
||||
return;
|
||||
|
||||
if (g_iLastHp >= 0)
|
||||
{
|
||||
if (hp > g_iLastHp)
|
||||
g_fHealUntil = GetGameTime() + HUD_HP_HEAL_HOLD;
|
||||
else if (hp < g_iLastHp)
|
||||
g_fHealUntil = 0.0;
|
||||
}
|
||||
|
||||
g_iLastHp = hp;
|
||||
|
||||
QueueFlush();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Sending
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
@@ -203,7 +280,7 @@ void Flush()
|
||||
return;
|
||||
}
|
||||
|
||||
if (BuildHudText(g_sHudText, sizeof(g_sHudText)) == 0)
|
||||
if (!BuildHudText(g_sHudText, sizeof(g_sHudText)))
|
||||
{
|
||||
ClearAll();
|
||||
return;
|
||||
@@ -215,8 +292,20 @@ void Flush()
|
||||
void Send()
|
||||
{
|
||||
// Held until the last name lapses, so the text leaves on its own if no update ever comes.
|
||||
// Names lapsing sooner are dropped by a resend from the expiry timer.
|
||||
SetHudTextParams(0.8, 0.2, g_fLastExpiry - GetGameTime(), 255, 255, 255, 255, 0, 0.0, 0.0, 0.0);
|
||||
// Names lapsing sooner are dropped by a resend from the expiry timer. The HP line outlives
|
||||
// the names and is kept up by the refresh instead.
|
||||
float fNow = GetGameTime();
|
||||
float fHold = g_fLastExpiry - fNow;
|
||||
|
||||
if (g_bHpLine)
|
||||
{
|
||||
if (fHold < HUD_HP_HOLD)
|
||||
fHold = HUD_HP_HOLD;
|
||||
|
||||
g_fNextHpRefresh = fNow + HUD_HP_REFRESH;
|
||||
}
|
||||
|
||||
SetHudTextParams(0.8, 0.2, fHold, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0);
|
||||
|
||||
for (int i = 1; i <= MaxClients; i++)
|
||||
{
|
||||
@@ -231,9 +320,10 @@ void Send()
|
||||
g_hExpireTimer = CreateTimer(HUD_EXPIRE_INTERVAL, Timer_Expire, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE);
|
||||
}
|
||||
|
||||
// Most recent shooters first. Returns how many players are listed, shown or not. Names are
|
||||
// packed until the message is full, then come off the bottom until a "+ N" line fits.
|
||||
int BuildHudText(char[] text, int maxlen)
|
||||
// HP line first when the couch is below full, then the most recent shooters. Returns whether there
|
||||
// is anything to show. Names are packed until the message is full, then come off the bottom until
|
||||
// a "+ N" line fits.
|
||||
bool BuildHudText(char[] text, int maxlen)
|
||||
{
|
||||
float fNow = GetGameTime();
|
||||
bool bTaken[MAXPLAYERS + 1];
|
||||
@@ -276,13 +366,26 @@ int BuildHudText(char[] text, int maxlen)
|
||||
g_fLastExpiry = g_fExpire[best];
|
||||
}
|
||||
|
||||
if (total == 0)
|
||||
g_bHpLine = (Couch_IsHpEnabled() && Couch_GetHp() < Couch_GetMaxHp());
|
||||
|
||||
text[0] = '\0';
|
||||
|
||||
if (g_bHpLine)
|
||||
{
|
||||
text[0] = '\0';
|
||||
return 0;
|
||||
// Solid while healing; the timer only flips it otherwise.
|
||||
if (fNow < g_fHealUntil)
|
||||
g_bHeartAlert = false;
|
||||
|
||||
char sHp[16], sMax[16];
|
||||
FormatThousands(Couch_GetHp(), sHp, sizeof(sHp));
|
||||
FormatThousands(Couch_GetMaxHp(), sMax, sizeof(sMax));
|
||||
FormatEx(text, maxlen, HUD_HP_HEART ... "%s %s/%s", g_bHeartAlert ? HUD_HP_HEART_ALERT : "", sHp, sMax);
|
||||
}
|
||||
|
||||
strcopy(text, maxlen, "Shooting couch:");
|
||||
if (total == 0)
|
||||
return g_bHpLine;
|
||||
|
||||
StrCat(text, maxlen, g_bHpLine ? "\nShooting couch:" : "Shooting couch:");
|
||||
|
||||
// rowStart[n] is the text length before row n, so a row can be cut back off.
|
||||
int rowStart[MAXPLAYERS + 1];
|
||||
@@ -294,8 +397,8 @@ int BuildHudText(char[] text, int maxlen)
|
||||
GetClientName(order[rows], sName, sizeof(sName));
|
||||
TruncateUtf8(sName, HUD_NAME_BYTES);
|
||||
|
||||
char sLine[HUD_NAME_BYTES + sizeof(HUD_GRENADE_TAG) + 1];
|
||||
FormatEx(sLine, sizeof(sLine), "\n%s%s", (g_fGrenadeUntil[order[rows]] > fNow) ? HUD_GRENADE_TAG : "", sName);
|
||||
char sLine[HUD_NAME_BYTES + sizeof(HUD_ZOMBIE_TAG) + sizeof(HUD_GRENADE_TAG) + 1];
|
||||
FormatEx(sLine, sizeof(sLine), "\n%s%s%s", g_bZombieHit[order[rows]] ? HUD_ZOMBIE_TAG : "", (g_fGrenadeUntil[order[rows]] > fNow) ? HUD_GRENADE_TAG : "", sName);
|
||||
|
||||
int len = strlen(text);
|
||||
if (len + strlen(sLine) >= maxlen)
|
||||
@@ -321,7 +424,7 @@ int BuildHudText(char[] text, int maxlen)
|
||||
text[rowStart[rows]] = '\0';
|
||||
}
|
||||
|
||||
return total;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cuts to at most maxBytes without leaving a partial multi-byte character on the end.
|
||||
@@ -347,8 +450,30 @@ public Action Timer_Expire(Handle timer)
|
||||
return Plugin_Stop;
|
||||
}
|
||||
|
||||
// Only resend when a name has lapsed.
|
||||
if (GetGameTime() >= g_fNextExpiry)
|
||||
// Only resend when a name has lapsed, the HP line is due its refresh, or the heart has to flip.
|
||||
// With nobody listed there is no expiry (0.0), so the guard stops the HP line being resent
|
||||
// every tick.
|
||||
float fNow = GetGameTime();
|
||||
bool bExpiry = (g_fNextExpiry > 0.0 && fNow >= g_fNextExpiry);
|
||||
bool bRefresh = (g_bHpLine && fNow >= g_fNextHpRefresh);
|
||||
bool bHeart = false;
|
||||
|
||||
if (g_bHpLine)
|
||||
{
|
||||
if (fNow >= g_fHealUntil)
|
||||
{
|
||||
// Not healing: alternate every tick.
|
||||
g_bHeartAlert = !g_bHeartAlert;
|
||||
bHeart = true;
|
||||
}
|
||||
else if (g_bHeartAlert)
|
||||
{
|
||||
// Healing started while "♥!" was up; put the solid heart back.
|
||||
bHeart = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (bExpiry || bRefresh || bHeart)
|
||||
Flush();
|
||||
|
||||
return Plugin_Continue;
|
||||
@@ -378,7 +503,14 @@ void ResetAll()
|
||||
g_fLastHit[i] = 0.0;
|
||||
g_fGrenadeUntil[i] = 0.0;
|
||||
g_fNextUseSend[i] = 0.0;
|
||||
g_bZombieHit[i] = false;
|
||||
}
|
||||
|
||||
g_bHpLine = false;
|
||||
g_fNextHpRefresh = 0.0;
|
||||
g_iLastHp = -1;
|
||||
g_fHealUntil = 0.0;
|
||||
g_bHeartAlert = false;
|
||||
|
||||
ClearAll();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,32 @@
|
||||
|
||||
#define _Couch_included
|
||||
|
||||
// Chat prefix shared by the whole couch plugin set. The trailing colour code tints the token that
|
||||
// follows it (a name or value); messages switch back with \x01. PREFIX_PLAIN is for console
|
||||
// replies, which don't render colour codes.
|
||||
#define PREFIX "\x07CD853F[Couch] \x04"
|
||||
#define PREFIX_PLAIN "[Couch]"
|
||||
|
||||
// Non-negative integer with a comma every three digits: 1532 -> "1,532".
|
||||
stock void FormatThousands(int value, char[] buffer, int maxlen)
|
||||
{
|
||||
char sDigits[16];
|
||||
int len = IntToString(value, sDigits, sizeof(sDigits));
|
||||
|
||||
int out = 0;
|
||||
for (int i = 0; i < len && out < maxlen - 1; i++)
|
||||
{
|
||||
// A comma goes before every digit that starts a new group of three from the right.
|
||||
if (i > 0 && (len - i) % 3 == 0)
|
||||
buffer[out++] = ',';
|
||||
|
||||
if (out < maxlen - 1)
|
||||
buffer[out++] = sDigits[i];
|
||||
}
|
||||
|
||||
buffer[out] = '\0';
|
||||
}
|
||||
|
||||
// How a player moved the couch, passed to Couch_OnCouchInteraction.
|
||||
enum CouchMethod
|
||||
{
|
||||
@@ -58,6 +84,25 @@ forward void Couch_OnCouchInteraction(int couch, int client, CouchMethod method,
|
||||
*/
|
||||
forward void Couch_OnClientBlocked(int couch, int client);
|
||||
|
||||
/**
|
||||
* Called whenever the couch's HP changes: zombie hits, regen ticks, admin sets, resets to full
|
||||
* on spawn and round start, and when the HP feature is switched on or off.
|
||||
*
|
||||
* @param couch Entity index of the couch, or -1 if the feature was switched off while there was none.
|
||||
* @param hp Current HP.
|
||||
* @param maxhp Maximum HP, sm_couch_hp_max.
|
||||
*/
|
||||
forward void Couch_OnCouchHpChanged(int couch, int hp, int maxhp);
|
||||
|
||||
/**
|
||||
* Called when the couch's HP reaches 0, before the humans are killed and the couch is removed.
|
||||
* Couch_OnCouchRemoved follows.
|
||||
*
|
||||
* @param couch Entity index of the couch; still valid during this call.
|
||||
* @param attacker Client index of the zombie whose hit broke it, or 0 when an admin set the HP to 0.
|
||||
*/
|
||||
forward void Couch_OnCouchBroken(int couch, int attacker);
|
||||
|
||||
/**
|
||||
* @return Entity index of the couch, or -1 if there is none.
|
||||
*/
|
||||
@@ -114,6 +159,16 @@ native bool Couch_IsEffectEnabled();
|
||||
*/
|
||||
native void Couch_SetEffectEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Whether the sm_beacon-style rings on the couch are on. Mirrors sm_couch_beacon.
|
||||
*/
|
||||
native bool Couch_IsBeaconEnabled();
|
||||
|
||||
/**
|
||||
* Switches the beacon rings on or off. Writes sm_couch_beacon.
|
||||
*/
|
||||
native void Couch_SetBeaconEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Whether only pistols and knives may move the couch. Mirrors sm_couch_pistol_only.
|
||||
*/
|
||||
@@ -151,6 +206,43 @@ native void Couch_SetClientBlocked(int client, bool blocked);
|
||||
*/
|
||||
native bool Couch_IsClientBlocked(int client);
|
||||
|
||||
/**
|
||||
* Whether couch HP is on: zombie hits wear the couch down and at 0 HP it breaks, killing every
|
||||
* human. Mirrors sm_couch_hp.
|
||||
*/
|
||||
native bool Couch_IsHpEnabled();
|
||||
|
||||
/**
|
||||
* Switches couch HP on or off. Turning it on sets the HP to full. Writes sm_couch_hp.
|
||||
*/
|
||||
native void Couch_SetHpEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @return Current couch HP, or 0 if there is no couch.
|
||||
*/
|
||||
native int Couch_GetHp();
|
||||
|
||||
/**
|
||||
* Sets the couch's HP, clamped to 0..Couch_GetMaxHp(). Setting 0 breaks the couch as a zombie
|
||||
* hit would, provided the HP feature is on and a couch exists.
|
||||
*
|
||||
* @param hp New HP.
|
||||
*/
|
||||
native void Couch_SetHp(int hp);
|
||||
|
||||
/**
|
||||
* @return Maximum couch HP. Mirrors sm_couch_hp_max.
|
||||
*/
|
||||
native int Couch_GetMaxHp();
|
||||
|
||||
/**
|
||||
* Sets the maximum couch HP. A couch at full HP stays full; otherwise the HP is clamped to the
|
||||
* new maximum. Writes sm_couch_hp_max.
|
||||
*
|
||||
* @param maxhp New maximum, at least 1.
|
||||
*/
|
||||
native void Couch_SetMaxHp(int maxhp);
|
||||
|
||||
|
||||
public SharedPlugin __pl_Couch =
|
||||
{
|
||||
@@ -175,12 +267,20 @@ public SharedPlugin __pl_Couch =
|
||||
MarkNativeAsOptional("Couch_Remove");
|
||||
MarkNativeAsOptional("Couch_IsEffectEnabled");
|
||||
MarkNativeAsOptional("Couch_SetEffectEnabled");
|
||||
MarkNativeAsOptional("Couch_IsBeaconEnabled");
|
||||
MarkNativeAsOptional("Couch_SetBeaconEnabled");
|
||||
MarkNativeAsOptional("Couch_IsPistolOnly");
|
||||
MarkNativeAsOptional("Couch_SetPistolOnly");
|
||||
MarkNativeAsOptional("Couch_IsUseAllowed");
|
||||
MarkNativeAsOptional("Couch_SetUseAllowed");
|
||||
MarkNativeAsOptional("Couch_SetClientBlocked");
|
||||
MarkNativeAsOptional("Couch_IsClientBlocked");
|
||||
MarkNativeAsOptional("Couch_IsHpEnabled");
|
||||
MarkNativeAsOptional("Couch_SetHpEnabled");
|
||||
MarkNativeAsOptional("Couch_GetHp");
|
||||
MarkNativeAsOptional("Couch_SetHp");
|
||||
MarkNativeAsOptional("Couch_GetMaxHp");
|
||||
MarkNativeAsOptional("Couch_SetMaxHp");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user