/*
* CSB Pets
* Copyright (C) 2026 counter-strike-boost.com
*
* A cosmetic pet model that follows the player. On spawn a non-solid model
* entity is created and, on a movement task, glides toward a point just behind
* and above its owner with smooth interpolation, always facing the owner. Each
* player picks a pet from a menu; the choice is stored per SteamID in nVault and
* restored on their next spawn. Pets are removed on death and disconnect.
*
* 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 Pets"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
new const g_szPetName[][] = { "None", "Scientist", "Chicken", "Robo Grunt" }
new const g_szPetModel[][] =
{
"",
"models/scientist.mdl",
"models/chicken.mdl",
"models/hgrunt.mdl"
}
new g_pEnabled, g_pSpeed
new g_iVault
new g_iPetEnt[33]
new g_iPetChoice[33]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_pets_enabled", "1")
g_pSpeed = register_cvar("csb_pets_speed", "0.25")
register_clcmd("say /pet", "cmdMenu")
register_clcmd("say_team /pet", "cmdMenu")
register_event("ResetHUD", "eventSpawn", "b")
register_event("DeathMsg", "eventDeath", "a", "1>0")
g_iVault = nvault_open("csb_pets")
set_task(0.1, "taskFollow", 0, _, _, "b")
}
public plugin_precache()
{
for (new i = 1; i < sizeof(g_szPetModel); i++)
{
if (g_szPetModel[i][0])
engfunc(EngFunc_PrecacheModel, g_szPetModel[i])
}
}
public client_putinserver(id)
{
g_iPetEnt[id] = 0
g_iPetChoice[id] = 0
loadChoice(id)
}
public client_disconnected(id)
{
removePet(id)
g_iPetChoice[id] = 0
}
loadChoice(id)
{
new auth[35]
get_user_authid(id, auth, charsmax(auth))
if (!auth[0])
return
new data[8], ts
if (nvault_lookup(g_iVault, auth, data, charsmax(data), ts))
g_iPetChoice[id] = clamp(str_to_num(data), 0, sizeof(g_szPetModel) - 1)
}
saveChoice(id)
{
new auth[35]
get_user_authid(id, auth, charsmax(auth))
if (!auth[0])
return
new data[8]
num_to_str(g_iPetChoice[id], data, charsmax(data))
nvault_set(g_iVault, auth, data)
}
public cmdMenu(id)
{
if (!get_pcvar_num(g_pEnabled))
return PLUGIN_HANDLED
new menu = menu_create("\yCSB Pets^n\wPick your pet:", "menuHandler")
new info[8]
for (new i = 0; i < sizeof(g_szPetName); i++)
{
num_to_str(i, info, charsmax(info))
menu_additem(menu, g_szPetName[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_iPetChoice[id] = clamp(str_to_num(info), 0, sizeof(g_szPetModel) - 1)
saveChoice(id)
removePet(id)
if (g_iPetChoice[id] == 0)
{
client_print(id, print_chat, "[CSB] Pet removed.")
}
else
{
client_print(id, print_chat, "[CSB] Pet set to %s.", g_szPetName[g_iPetChoice[id]])
if (is_user_alive(id))
spawnPet(id)
}
return PLUGIN_HANDLED
}
public eventSpawn(id)
{
if (is_user_alive(id) && g_iPetChoice[id] > 0)
spawnPet(id)
}
public eventDeath()
{
new victim = read_data(2)
if (victim >= 1 && victim <= 32)
removePet(victim)
}
spawnPet(id)
{
removePet(id)
new choice = g_iPetChoice[id]
if (choice <= 0 || !g_szPetModel[choice][0])
return
new ent = engfunc(EngFunc_CreateNamedEntity, engfunc(EngFunc_AllocString, "info_target"))
if (!ent)
return
set_pev(ent, pev_classname, "csb_pet")
set_pev(ent, pev_owner, id)
set_pev(ent, pev_movetype, MOVETYPE_NOCLIP)
set_pev(ent, pev_solid, SOLID_NOT)
engfunc(EngFunc_SetModel, ent, g_szPetModel[choice])
static Float:fOrigin[3]
pev(id, pev_origin, fOrigin)
engfunc(EngFunc_SetOrigin, ent, fOrigin)
g_iPetEnt[id] = ent
}
removePet(id)
{
new ent = g_iPetEnt[id]
if (ent > 0 && pev_valid(ent))
engfunc(EngFunc_RemoveEntity, ent)
g_iPetEnt[id] = 0
}
public taskFollow()
{
new players[32], num
get_players(players, num, "a")
new Float:speed = get_pcvar_float(g_pSpeed)
if (speed <= 0.0)
speed = 0.25
else if (speed > 1.0)
speed = 1.0
for (new i = 0; i < num; i++)
{
new id = players[i]
new ent = g_iPetEnt[id]
if (ent <= 0 || !pev_valid(ent))
continue
static Float:fOwner[3], Float:fAng[3], Float:fFwd[3], Float:fTarget[3], Float:fPet[3]
pev(id, pev_origin, fOwner)
pev(id, pev_angles, fAng)
fAng[0] = 0.0
angle_vector(fAng, ANGLEVECTOR_FORWARD, fFwd)
/* point behind and slightly above the owner */
fTarget[0] = fOwner[0] - fFwd[0] * 60.0
fTarget[1] = fOwner[1] - fFwd[1] * 60.0
fTarget[2] = fOwner[2] + 20.0
pev(ent, pev_origin, fPet)
static Float:fNew[3]
fNew[0] = fPet[0] + (fTarget[0] - fPet[0]) * speed
fNew[1] = fPet[1] + (fTarget[1] - fPet[1]) * speed
fNew[2] = fPet[2] + (fTarget[2] - fPet[2]) * speed
engfunc(EngFunc_SetOrigin, ent, fNew)
/* face the owner */
static Float:fLook[3]
fLook[0] = fOwner[0] - fNew[0]
fLook[1] = fOwner[1] - fNew[1]
fLook[2] = 0.0
static Float:fYaw
fYaw = floatatan2(fLook[1], fLook[0], degrees)
static Float:fSet[3]
fSet[0] = 0.0
fSet[1] = fYaw
fSet[2] = 0.0
set_pev(ent, pev_angles, fSet)
}
}