/*
* CSB Portal Gun
* Copyright (C) 2026 counter-strike-boost.com
*
* A two-portal teleporter. Bind a key to +portalblue and one to +portalorange;
* each fires a trace from your aim and drops that colour portal on the surface
* you hit. When both of your portals exist, walking into one teleports you to
* the other while keeping your velocity, so you shoot out the far side. Portals
* are per-player, marked with a coloured sprite, and cleared at round end.
*
* 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
new const PLUGIN[] = "CSB Portal Gun"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
new g_pEnabled, g_pRadius
new bool:g_bHas[33][2]
new Float:g_fPortal[33][2][3]
new Float:g_fTpCooldown[33]
new g_iSpr
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_portal_enabled", "1")
g_pRadius = register_cvar("csb_portal_radius", "48.0")
register_clcmd("+portalblue", "cmdBlue")
register_clcmd("+portalorange", "cmdOrange")
register_logevent("eventRoundStart", 2, "1=Round_Start")
set_task(0.1, "taskPortals", 0, _, _, "b")
}
public plugin_precache()
{
g_iSpr = precache_model("sprites/laserbeam.spr")
}
public client_putinserver(id)
{
resetPortals(id)
}
resetPortals(id)
{
g_bHas[id][0] = false
g_bHas[id][1] = false
g_fTpCooldown[id] = 0.0
}
public eventRoundStart()
{
for (new id = 1; id <= 32; id++)
resetPortals(id)
}
public cmdBlue(id)
{
placePortal(id, 0)
return PLUGIN_HANDLED
}
public cmdOrange(id)
{
placePortal(id, 1)
return PLUGIN_HANDLED
}
placePortal(id, slot)
{
if (!get_pcvar_num(g_pEnabled) || !is_user_alive(id))
return
static Float:fEye[3], Float:fOfs[3], Float:fAim[3], Float:fFwd[3], Float:fEnd[3]
pev(id, pev_origin, fEye)
pev(id, pev_view_ofs, fOfs)
fEye[0] += fOfs[0]
fEye[1] += fOfs[1]
fEye[2] += fOfs[2]
pev(id, pev_v_angle, fAim)
angle_vector(fAim, ANGLEVECTOR_FORWARD, fFwd)
fEnd[0] = fEye[0] + fFwd[0] * 8192.0
fEnd[1] = fEye[1] + fFwd[1] * 8192.0
fEnd[2] = fEye[2] + fFwd[2] * 8192.0
engfunc(EngFunc_TraceLine, fEye, fEnd, IGNORE_MONSTERS, id, 0)
static Float:fHit[3], Float:fNormal[3], Float:fFrac
get_tr2(0, TR_flFraction, fFrac)
get_tr2(0, TR_vecEndPos, fHit)
get_tr2(0, TR_vecPlaneNormal, fNormal)
if (fFrac >= 1.0)
{
client_print(id, print_chat, "[CSB] No surface in range for the portal.")
return
}
/* nudge the portal off the surface so the player can step into it */
fHit[0] += fNormal[0] * 20.0
fHit[1] += fNormal[1] * 20.0
fHit[2] += fNormal[2] * 20.0
g_fPortal[id][slot][0] = fHit[0]
g_fPortal[id][slot][1] = fHit[1]
g_fPortal[id][slot][2] = fHit[2]
g_bHas[id][slot] = true
client_print(id, print_chat, "[CSB] %s portal placed.", slot == 0 ? "Blue" : "Orange")
}
public taskPortals()
{
new players[32], num
get_players(players, num, "a")
new Float:radius = get_pcvar_float(g_pRadius)
new Float:r2 = radius * radius
new Float:now = get_gametime()
for (new i = 0; i < num; i++)
{
new id = players[i]
/* draw whichever portals exist */
if (g_bHas[id][0])
drawPortal(g_fPortal[id][0], 0, 90, 255)
if (g_bHas[id][1])
drawPortal(g_fPortal[id][1], 255, 140, 0)
if (!g_bHas[id][0] || !g_bHas[id][1])
continue
if (now < g_fTpCooldown[id])
continue
static Float:fMe[3]
pev(id, pev_origin, fMe)
if (dist2(fMe, g_fPortal[id][0]) < r2)
{
teleport(id, g_fPortal[id][1])
g_fTpCooldown[id] = now + 1.0
}
else if (dist2(fMe, g_fPortal[id][1]) < r2)
{
teleport(id, g_fPortal[id][0])
g_fTpCooldown[id] = now + 1.0
}
}
}
teleport(id, const Float:dest[3])
{
static Float:fVel[3]
pev(id, pev_velocity, fVel)
engfunc(EngFunc_SetOrigin, id, dest)
set_pev(id, pev_velocity, fVel)
/* teleport sparkle */
message_begin(MSG_PVS, SVC_TEMPENTITY, dest)
write_byte(TE_TELEPORT)
engfunc(EngFunc_WriteCoord, dest[0])
engfunc(EngFunc_WriteCoord, dest[1])
engfunc(EngFunc_WriteCoord, dest[2])
message_end()
}
drawPortal(const Float:pos[3], r, g, b)
{
static Float:top[3]
top[0] = pos[0]
top[1] = pos[1]
top[2] = pos[2] + 40.0
message_begin(MSG_BROADCAST, SVC_TEMPENTITY)
write_byte(TE_BEAMPOINTS)
engfunc(EngFunc_WriteCoord, pos[0])
engfunc(EngFunc_WriteCoord, pos[1])
engfunc(EngFunc_WriteCoord, pos[2] - 40.0)
engfunc(EngFunc_WriteCoord, top[0])
engfunc(EngFunc_WriteCoord, top[1])
engfunc(EngFunc_WriteCoord, top[2])
write_short(g_iSpr)
write_byte(0)
write_byte(0)
write_byte(2)
write_byte(30)
write_byte(0)
write_byte(r)
write_byte(g)
write_byte(b)
write_byte(200)
write_byte(0)
message_end()
}
Float:dist2(const Float:a[3], const Float:b[3])
{
new Float:dx = a[0] - b[0]
new Float:dy = a[1] - b[1]
new Float:dz = a[2] - b[2]
return dx * dx + dy * dy + dz * dz
}