/* * 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) } }