/*
* CSB Player Trails
* Copyright (C) 2026 counter-strike-boost.com
*
* Attaches a coloured beam-follow trail to a player. A colour is picked from a
* menu (VIP/shop gated) and the TE_BEAMFOLLOW temp entity is re-sent on a task
* so the trail persists as the player moves. Trails are cleared on death and at
* round start so the arena does not fill up with streaks.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version. It is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See for details.
*/
#include
new const PLUGIN[] = "CSB Player Trails"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define ACCESS_FLAG ADMIN_LEVEL_H
new const g_szColorName[][] = { "Off", "Red", "Green", "Blue", "Yellow", "Cyan", "Purple", "White" }
new const g_iColor[][3] =
{
{ 0, 0, 0 },
{ 255, 0, 0 },
{ 0, 255, 0 },
{ 40, 90, 255 },
{ 255, 255, 0 },
{ 0, 255, 255 },
{ 200, 0, 255 },
{ 255, 255, 255 }
}
new g_pEnabled
new g_iTrailSpr
new g_iChoice[33]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_trails_enabled", "1")
register_clcmd("say /trail", "cmdMenu")
register_clcmd("say_team /trail", "cmdMenu")
register_event("DeathMsg", "eventDeath", "a", "1>0")
register_logevent("eventRoundStart", 2, "1=Round_Start")
set_task(1.0, "taskTrails", 0, _, _, "b")
}
public plugin_precache()
{
g_iTrailSpr = precache_model("sprites/laserbeam.spr")
}
public client_putinserver(id)
{
g_iChoice[id] = 0
}
bool:hasAccess(id)
{
return (get_user_flags(id) & ACCESS_FLAG) != 0
}
public cmdMenu(id)
{
if (!get_pcvar_num(g_pEnabled))
return PLUGIN_HANDLED
if (!hasAccess(id))
{
client_print(id, print_chat, "[CSB] Trails are a VIP feature.")
return PLUGIN_HANDLED
}
new menu = menu_create("\yCSB Trail^n\wPick a colour:", "menuHandler")
new info[8]
for (new i = 0; i < sizeof(g_szColorName); i++)
{
num_to_str(i, info, charsmax(info))
menu_additem(menu, g_szColorName[i], info, 0)
}
menu_setprop(menu, MPROP_EXIT, MEXIT_ALL)
menu_display(id, menu, 0)
return PLUGIN_HANDLED
}
public menuHandler(id, menu, item)
{
if (item == MENU_EXIT)
{
menu_destroy(menu)
return PLUGIN_HANDLED
}
new info[8], name[32], access, callback
menu_item_getinfo(menu, item, access, info, charsmax(info), name, charsmax(name), callback)
menu_destroy(menu)
g_iChoice[id] = clamp(str_to_num(info), 0, sizeof(g_szColorName) - 1)
killTrail(id)
if (g_iChoice[id] == 0)
client_print(id, print_chat, "[CSB] Trail removed.")
else
{
client_print(id, print_chat, "[CSB] Trail set to %s.", g_szColorName[g_iChoice[id]])
if (is_user_alive(id))
sendTrail(id)
}
return PLUGIN_HANDLED
}
public eventDeath()
{
new victim = read_data(2)
if (victim >= 1 && victim <= 32)
killTrail(victim)
}
public eventRoundStart()
{
new players[32], num
get_players(players, num, "c")
for (new i = 0; i < num; i++)
killTrail(players[i])
}
sendTrail(id)
{
if (!is_user_alive(id) || g_iChoice[id] == 0)
return
new c = g_iChoice[id]
message_begin(MSG_BROADCAST, SVC_TEMPENTITY)
write_byte(TE_BEAMFOLLOW)
write_short(id)
write_short(g_iTrailSpr)
write_byte(20) /* life in 0.1s */
write_byte(6) /* width */
write_byte(g_iColor[c][0])
write_byte(g_iColor[c][1])
write_byte(g_iColor[c][2])
write_byte(180) /* brightness */
message_end()
}
killTrail(id)
{
/* a zero-life beam follow on the entity clears any active one */
message_begin(MSG_BROADCAST, SVC_TEMPENTITY)
write_byte(TE_KILLBEAM)
write_short(id)
message_end()
}
public taskTrails()
{
new players[32], num
get_players(players, num, "a")
for (new i = 0; i < num; i++)
{
new id = players[i]
if (g_iChoice[id] > 0)
sendTrail(id)
}
}