/*
* CSB Cluster Grenade
* Copyright (C) 2026 counter-strike-boost.com
*
* Upgrades the HE grenade into a cluster bomb: when the parent grenade goes off
* it scatters N smaller bomblets with random velocities; each bomblet arms for a
* short fuse and then explodes on its own, dealing scaled blast damage in a
* radius. Great for chaos rounds and fun modes.
*
* Inspired by Cluster Grenade by VEN. Independent GPL re-implementation; no
* original code is reused.
*
* 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
#include
#include
new const PLUGIN[] = "CSB Cluster Grenade"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define TE_EXPLOSION 3
#define BOMBLET_CLASS "csb_bomblet"
new const g_szHeModel[] = "models/w_hegrenade.mdl"
new const g_szBombletModel[] = "models/w_hegrenade.mdl"
new g_pEnabled, g_pCount, g_pDamage, g_pRadius, g_pFuse
new g_iSprExplode
new g_iMaxPlayers
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_cluster_enabled", "1")
g_pCount = register_cvar("csb_cluster_count", "6")
g_pDamage = register_cvar("csb_cluster_damage", "60")
g_pRadius = register_cvar("csb_cluster_radius", "160")
g_pFuse = register_cvar("csb_cluster_fuse", "1.0")
register_forward(FM_SetModel, "fwSetModel")
register_forward(FM_Think, "fwThink")
g_iMaxPlayers = get_maxplayers()
}
public plugin_precache()
{
g_iSprExplode = precache_model("sprites/zerogxplode.spr")
}
public fwSetModel(ent, const model[])
{
if (!get_pcvar_num(g_pEnabled) || !pev_valid(ent))
return FMRES_IGNORED
if (!equal(model, g_szHeModel))
return FMRES_IGNORED
new Float:fDmgTime
pev(ent, pev_dmgtime, fDmgTime)
/* fire the cluster split just as the parent detonates */
new Float:delay = fDmgTime - get_gametime() - 0.05
if (delay < 0.2)
delay = 0.2
set_task(delay, "taskSplit", ent)
return FMRES_IGNORED
}
public taskSplit(ent)
{
if (!pev_valid(ent))
return
static Float:origin[3]
pev(ent, pev_origin, origin)
new owner = pev(ent, pev_owner)
new count = get_pcvar_num(g_pCount)
new Float:fuse = get_pcvar_float(g_pFuse)
for (new i = 0; i < count; i++)
spawnBomblet(origin, owner, fuse)
}
spawnBomblet(const Float:origin[3], owner, Float:fuse)
{
new ent = engfunc(EngFunc_CreateNamedEntity, engfunc(EngFunc_AllocString, "info_target"))
if (!pev_valid(ent))
return
set_pev(ent, pev_classname, BOMBLET_CLASS)
engfunc(EngFunc_SetModel, ent, g_szBombletModel)
set_pev(ent, pev_movetype, MOVETYPE_BOUNCE)
set_pev(ent, pev_solid, SOLID_BBOX)
set_pev(ent, pev_owner, owner)
set_pev(ent, pev_gravity, 1.0)
static Float:mins[3], Float:maxs[3]
mins[0] = -2.0; mins[1] = -2.0; mins[2] = -2.0
maxs[0] = 2.0; maxs[1] = 2.0; maxs[2] = 2.0
engfunc(EngFunc_SetSize, ent, mins, maxs)
static Float:spawn[3]
spawn[0] = origin[0]
spawn[1] = origin[1]
spawn[2] = origin[2] + 8.0
engfunc(EngFunc_SetOrigin, ent, spawn)
static Float:vel[3]
vel[0] = float(random_num(-220, 220))
vel[1] = float(random_num(-220, 220))
vel[2] = float(random_num(150, 350))
set_pev(ent, pev_velocity, vel)
set_pev(ent, pev_nextthink, get_gametime() + fuse + random_float(0.0, 0.4))
}
public fwThink(ent)
{
if (!pev_valid(ent))
return FMRES_IGNORED
static cls[32]
pev(ent, pev_classname, cls, charsmax(cls))
if (!equal(cls, BOMBLET_CLASS))
return FMRES_IGNORED
static origin[3], Float:fOrigin[3]
pev(ent, pev_origin, fOrigin)
origin[0] = floatround(fOrigin[0])
origin[1] = floatround(fOrigin[1])
origin[2] = floatround(fOrigin[2])
new owner = pev(ent, pev_owner)
message_begin_f(MSG_PVS, SVC_TEMPENTITY, fOrigin)
write_byte(TE_EXPLOSION)
write_coord(origin[0])
write_coord(origin[1])
write_coord(origin[2])
write_short(g_iSprExplode)
write_byte(10)
write_byte(18)
write_byte(0)
message_end()
blastDamage(fOrigin, owner)
engfunc(EngFunc_RemoveEntity, ent)
return FMRES_IGNORED
}
blastDamage(const Float:origin[3], owner)
{
new Float:radius = get_pcvar_float(g_pRadius)
new Float:maxDmg = get_pcvar_float(g_pDamage)
new attacker = (owner >= 1 && owner <= g_iMaxPlayers) ? owner : 0
for (new i = 1; i <= g_iMaxPlayers; i++)
{
if (!is_user_alive(i))
continue
static Float:pOrigin[3]
pev(i, pev_origin, pOrigin)
new Float:dist = get_distance_f(origin, pOrigin)
if (dist > radius)
continue
new Float:scale = 1.0 - (dist / radius)
new Float:dmg = maxDmg * scale
if (dmg < 1.0)
continue
new inflictor = (attacker > 0) ? attacker : i
ExecuteHamB(Ham_TakeDamage, i, inflictor, inflictor, dmg, DMG_BLAST)
}
}