Couch changes

This commit is contained in:
2026-10-14 16:53:45 +01:00
parent fa8d12ddb5
commit fb971adf6d
5 changed files with 1089 additions and 98 deletions
+550 -31
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+100
View File
@@ -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