/*
* CSB Zombie Classes
* Copyright (C) 2026 counter-strike-boost.com
*
* A class system that sits on top of CSB Zombie Core. Players pick a zombie
* class from a menu (Classic, Runner, Tank, Leaper, Hunter); each class has its
* own health, speed, gravity, knockback and model. The choice is saved per
* player in nVault and applied automatically the moment they are infected,
* through the core's csb_zombie_infected forward. Bullet knockback is scaled
* by the class's knockback multiplier.
*
* Requires CSB Zombie Core (provides csb_is_zombie and the infection forward).
* Inspired by Zombie Plague Classes by MeRcyLeZZ. Independent GPL re-write.
*
* 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. Distributed WITHOUT ANY WARRANTY. See the GNU General
* Public License for more details: .
*/
#include
#include
#include
#include
#include
#include
// provided by CSB Zombie Core at runtime
native csb_is_zombie(id)
new const PLUGIN[] = "CSB Zombie Classes"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
// Class table: name, health, max speed, gravity, knockback, model.
new const g_szClassName[][] = { "Classic", "Runner", "Tank", "Leaper", "Hunter" }
new const g_szClassDesc[][] =
{
"Balanced all-rounder",
"Fast but fragile",
"Slow wall of health",
"Low gravity, big jumps",
"High speed, high knockback taken"
}
new const g_iClassHp[] = { 2000, 1400, 4000, 1600, 1800 }
new const Float:g_fClassSpeed[] = { 250.0, 320.0, 220.0, 260.0, 300.0 }
new const Float:g_fClassGravity[] = { 0.85, 0.80, 1.00, 0.55, 0.80 }
new const Float:g_fClassKnock[] = { 1.0, 1.4, 0.6, 1.2, 1.6 }
new const g_szClassModel[][] =
{
"zombie_source", "zombie_source", "zombie_source", "zombie_source", "zombie_source"
}
new g_pEnabled
new g_iClass[33]
new g_iVault
new g_iMaxPlayers
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_zmclass_enabled", "1")
register_clcmd("say /zclass", "cmdClassMenu")
register_clcmd("say /class", "cmdClassMenu")
RegisterHam(Ham_TakeDamage, "player", "fwTakeDamage", 0)
g_iMaxPlayers = get_maxplayers()
g_iVault = nvault_open("csb_zm_classes")
}
public plugin_end()
{
if (g_iVault != INVALID_HANDLE)
nvault_close(g_iVault)
}
public client_putinserver(id)
{
g_iClass[id] = loadClass(id)
}
// Called by CSB Zombie Core when a player becomes a zombie.
public csb_zombie_infected(id, infector)
{
if (!get_pcvar_num(g_pEnabled) || !is_user_alive(id))
return
applyClass(id)
}
applyClass(id)
{
new c = g_iClass[id]
if (c < 0 || c >= sizeof(g_szClassName))
c = 0
set_user_health(id, g_iClassHp[c])
set_user_maxspeed(id, g_fClassSpeed[c])
set_user_gravity(id, g_fClassGravity[c])
if (g_szClassModel[c][0])
cs_set_user_model(id, g_szClassModel[c])
client_print(id, print_center, "Zombie class: %s", g_szClassName[c])
}
public cmdClassMenu(id)
{
if (!get_pcvar_num(g_pEnabled))
return PLUGIN_HANDLED
new menu = menu_create("\yChoose your zombie class", "classHandler")
new line[96], info[8]
for (new i = 0; i < sizeof(g_szClassName); i++)
{
formatex(info, charsmax(info), "%d", i)
formatex(line, charsmax(line), "%s \r[HP %d]^n\w%s", g_szClassName[i], g_iClassHp[i], g_szClassDesc[i])
menu_additem(menu, line, info, 0)
}
menu_setprop(menu, MPROP_EXITNAME, "Close")
menu_display(id, menu, 0)
return PLUGIN_HANDLED
}
public classHandler(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)
new c = str_to_num(info)
if (c < 0 || c >= sizeof(g_szClassName))
return PLUGIN_HANDLED
g_iClass[id] = c
saveClass(id, c)
client_print(id, print_chat, "[Zombie] Class set to %s. It applies the next time you are infected.", g_szClassName[c])
// If already a zombie this round, apply immediately.
if (is_user_alive(id) && csb_is_zombie(id))
applyClass(id)
return PLUGIN_HANDLED
}
public fwTakeDamage(victim, inflictor, attacker, Float:damage, damagebits)
{
if (!get_pcvar_num(g_pEnabled))
return HAM_IGNORED
if (attacker < 1 || attacker > g_iMaxPlayers || attacker == victim)
return HAM_IGNORED
// only human-on-zombie bullet damage produces knockback
if (!csb_is_zombie(victim) || csb_is_zombie(attacker))
return HAM_IGNORED
if (!is_user_alive(victim))
return HAM_IGNORED
new c = g_iClass[victim]
if (c < 0 || c >= sizeof(g_szClassName))
c = 0
new Float:kb = g_fClassKnock[c]
if (kb <= 0.0)
return HAM_IGNORED
new Float:oa[3], Float:ov[3], Float:dir[3]
pev(attacker, pev_origin, oa)
pev(victim, pev_origin, ov)
dir[0] = ov[0] - oa[0]
dir[1] = ov[1] - oa[1]
new Float:len = floatsqroot(dir[0] * dir[0] + dir[1] * dir[1])
if (len <= 0.0)
return HAM_IGNORED
new Float:power = damage * kb
new Float:vel[3]
pev(victim, pev_velocity, vel)
vel[0] += dir[0] / len * power
vel[1] += dir[1] / len * power
set_pev(victim, pev_velocity, vel)
return HAM_IGNORED
}
saveClass(id, c)
{
new authid[35], data[8]
get_user_authid(id, authid, charsmax(authid))
formatex(data, charsmax(data), "%d", c)
nvault_set(g_iVault, authid, data)
}
loadClass(id)
{
new authid[35], data[8], timestamp
get_user_authid(id, authid, charsmax(authid))
if (nvault_lookup(g_iVault, authid, data, charsmax(data), timestamp))
return clamp(str_to_num(data), 0, sizeof(g_szClassName) - 1)
return 0
}