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;
}