This article is the documentation for the game "Team Fortress 2". Click here to go to the main page of the development documents.

Team Fortress 2/Scripting/VScript Examples: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
m (Nesciuse moved page Team Fortress 2/Scripting/VScript Examples/en to Team Fortress 2/Scripting/VScript Examples without leaving a redirect: Move en subpage to basepage)
(The Great Semicolon Massacre)
Line 1: Line 1:
{{LanguageBar}}
{{LanguageBar}}
{{TF2 topicon|docs}}
{{TF2 topicon|docs}}
{{tf2}} This page contains examples of [[Vscript|vscripts]] for {{tf2|3}}.
{{tf2}} This page contains examples of [[Vscript|vscripts]] for {{tf2|3}}.


Line 29: Line 27:


<source lang=js>
<source lang=js>
::MaxPlayers <- MaxClients().tointeger();
::MaxPlayers <- MaxClients().tointeger()


for (local i = 1; i <= MaxPlayers ; i++)
for (local i = 1; i <= MaxPlayers ; i++)
Line 43: Line 41:


<source lang=js>
<source lang=js>
::MAX_WEAPONS <- 8;
::MAX_WEAPONS <- 8


for (local i = 0; i < MAX_WEAPONS; i++)
for (local i = 0; i < MAX_WEAPONS; i++)
{
{
     local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
     local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
     if (weapon == null)
     if (weapon == null)
         continue;
         continue
     printl(weapon);
     printl(weapon)
}
}
</source>
</source>
Line 61: Line 59:
{
{
     if (wearable.GetClassname() != "tf_wearable")
     if (wearable.GetClassname() != "tf_wearable")
         continue;
         continue
     printl(wearable);
     printl(wearable)
}
}


Line 197: Line 195:
{
{
     // "self" is the player entity here
     // "self" is the player entity here
     self.AddCustomAttribute("no jump", 1, -1);
     self.AddCustomAttribute("no jump", 1, -1)
}
}


function OnGameEvent_player_spawn(params)
function OnGameEvent_player_spawn(params)
{
{
     local player = GetPlayerFromUserID(params.userid);
     local player = GetPlayerFromUserID(params.userid)
     if (player != null)
     if (player != null)
     {
     {
         EntFireByHandle(player, "CallScriptFunction", "PostPlayerSpawn", 0, null, null);
         EntFireByHandle(player, "CallScriptFunction", "PostPlayerSpawn", 0, null, null)
     }
     }
}
}
Line 222: Line 220:
function PlayerThink()
function PlayerThink()
{
{
local buttons = NetProps.GetPropInt(self, "m_nButtons");
local buttons = NetProps.GetPropInt(self, "m_nButtons")
if (buttons & Constants.FButtons.IN_RELOAD)
if (buttons & Constants.FButtons.IN_RELOAD)
{
{
printl("Player is reloading");
printl("Player is reloading")
}
}
return -1;
return -1
}
}


local player = GetListenServerHost();
local player = GetListenServerHost()
AddThinkToEnt(player, "PlayerThink");
AddThinkToEnt(player, "PlayerThink")
</source>
</source>


Line 247: Line 245:
function PlayerThink()
function PlayerThink()
{
{
local buttons = NetProps.GetPropInt(self, "m_nButtons");
local buttons = NetProps.GetPropInt(self, "m_nButtons")
local buttons_changed = buttons_last ^ buttons;
local buttons_changed = buttons_last ^ buttons
local buttons_pressed = buttons_changed & buttons;
local buttons_pressed = buttons_changed & buttons
local buttons_released = buttons_changed & (~buttons);
local buttons_released = buttons_changed & (~buttons)
if (buttons_pressed & Constants.FButtons.IN_ATTACK)
if (buttons_pressed & Constants.FButtons.IN_ATTACK)
{
{
printl("Player pressed left-click");
printl("Player pressed left-click")
}
}
buttons_last = buttons;
buttons_last = buttons
return -1;
return -1
}
}


local player = GetListenServerHost();
local player = GetListenServerHost()
player.ValidateScriptScope();
player.ValidateScriptScope()
player.GetScriptScope().buttons_last <- 0;
player.GetScriptScope().buttons_last <- 0
AddThinkToEnt(player, "PlayerThink");
AddThinkToEnt(player, "PlayerThink")
</source>
</source>


Line 319: Line 317:
     // 2 - dead
     // 2 - dead
     // 3 - respawnable (spectating)
     // 3 - respawnable (spectating)
     return NetProps.GetPropInt(entity, "m_lifeState") == 0;
     return NetProps.GetPropInt(entity, "m_lifeState") == 0
}
}
</source>
</source>
Line 329: Line 327:
::GetPlayerName <- function(player)
::GetPlayerName <- function(player)
{
{
     return NetProps.GetPropString(player, "m_szNetname");
     return NetProps.GetPropString(player, "m_szNetname")
}
}


::GetPlayerSteamID <- function(player)
::GetPlayerSteamID <- function(player)
{
{
     return NetProps.GetPropString(player, "m_szNetworkIDString");
     return NetProps.GetPropString(player, "m_szNetworkIDString")
}
}
</source>
</source>
Line 343: Line 341:
::SetEntityColor <- function(entity, r, g, b, a)
::SetEntityColor <- function(entity, r, g, b, a)
{
{
     local color = (r) | (g << 8) | (b << 16) | (a << 24);
     local color = (r) | (g << 8) | (b << 16) | (a << 24)
     NetProps.SetPropInt(entity, "m_clrRender", color);
     NetProps.SetPropInt(entity, "m_clrRender", color)
}
}


::GetEntityColor <- function(entity)
::GetEntityColor <- function(entity)
{
{
     local color = NetProps.GetPropInt(entity, "m_clrRender");
     local color = NetProps.GetPropInt(entity, "m_clrRender")
     return
     return
     {
     {
Line 356: Line 354:
         b = (color >> 16) & 0xFF,
         b = (color >> 16) & 0xFF,
         a = (color >> 24) & 0xFF,
         a = (color >> 24) & 0xFF,
     };
     }
}
}
</source>
</source>
Line 364: Line 362:
{{warning|This will cause a client crash when giving an Engineer toolbox ([[tf_weapon_builder]] with itemID 28). To fix this, you must set the <code>m_iObjectType</code> and <code>m_iObjectMode</code> netprops to 0 before equipping. You also need to set the <code>m_aBuildableObjectTypes</code> netprop array to allow objects to be built.}}
{{warning|This will cause a client crash when giving an Engineer toolbox ([[tf_weapon_builder]] with itemID 28). To fix this, you must set the <code>m_iObjectType</code> and <code>m_iObjectMode</code> netprops to 0 before equipping. You also need to set the <code>m_aBuildableObjectTypes</code> netprop array to allow objects to be built.}}
<source lang=js>
<source lang=js>
::MAX_WEAPONS <- 8;
::MAX_WEAPONS <- 8


::GivePlayerWeapon <- function(player, className, itemID)
::GivePlayerWeapon <- function(player, className, itemID)
{
{
     local weapon = Entities.CreateByClassname(className);
     local weapon = Entities.CreateByClassname(className)
     NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", itemID);
     NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", itemID)
     NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true);
     NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
     NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true);
     NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true)
     weapon.SetTeam(player.GetTeam());
     weapon.SetTeam(player.GetTeam())
     Entities.DispatchSpawn(weapon);   
     weapon.DispatchSpawn()


     // remove existing weapon in same slot
     // remove existing weapon in same slot
     for (local i = 0; i < MAX_WEAPONS; i++)
     for (local i = 0; i < MAX_WEAPONS; i++)
     {
     {
         local heldWeapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
         local heldWeapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
         if (heldWeapon == null)
         if (heldWeapon == null)
             continue;
             continue
         if (heldWeapon.GetSlot() != weapon.GetSlot())
         if (heldWeapon.GetSlot() != weapon.GetSlot())
             continue;
             continue
         heldWeapon.Destroy();
         heldWeapon.Destroy()
         NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i);
         NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i)
         break;
         break
     }
     }
      
      
     player.Weapon_Equip(weapon);     
     player.Weapon_Equip(weapon)
     player.Weapon_Switch(weapon);
     player.Weapon_Switch(weapon)


     return weapon;
     return weapon
}
}


GivePlayerWeapon(GetListenServerHost(), "tf_weapon_smg", 16);
GivePlayerWeapon(GetListenServerHost(), "tf_weapon_smg", 16)
</source>
</source>


Line 402: Line 400:
Example usage:
Example usage:
<source lang=js>
<source lang=js>
CreateRune(Vector(0, 0, -100), QAngle(0, 0, 0), Vector(-500, 0, 0), Constants.ETFTeam.TEAM_ANY, ERuneTypes.RUNE_VAMPIRE, false);
CreateRune(Vector(0, 0, -100), QAngle(0, 0, 0), Vector(-500, 0, 0), Constants.ETFTeam.TEAM_ANY, ERuneTypes.RUNE_VAMPIRE, false)
</source>
</source>
<br>
<br>
Line 422: Line 420:
     RUNE_PLAGUE,
     RUNE_PLAGUE,
     RUNE_SUPERNOVA,
     RUNE_SUPERNOVA,
};
}


::RuneTypeToCond <-
::RuneTypeToCond <-
Line 438: Line 436:
Constants.ETFCond.TF_COND_RUNE_PLAGUE,
Constants.ETFCond.TF_COND_RUNE_PLAGUE,
Constants.ETFCond.TF_COND_RUNE_SUPERNOVA,
Constants.ETFCond.TF_COND_RUNE_SUPERNOVA,
];
]


::MaxPlayers <- MaxClients().tointeger();
::MaxPlayers <- MaxClients().tointeger()


::CreateRune <- function(origin, angles, velocity, team, type, reposition)
::CreateRune <- function(origin, angles, velocity, team, type, reposition)
Line 446: Line 444:
// select random player to create a rune from
// select random player to create a rune from
// prioritize players with no rune, as stripping the rune temporarily can have side effects
// prioritize players with no rune, as stripping the rune temporarily can have side effects
local player, fallback;
local player, fallback
for (local i = 1; i <= MaxPlayers; i++)
for (local i = 1; i <= MaxPlayers; i++)
{
{
player = PlayerInstanceFromIndex(i);
player = PlayerInstanceFromIndex(i)
if (player)
if (player)
{
{
if (player.IsCarryingRune())
if (player.IsCarryingRune())
{
{
fallback = player;
fallback = player
player = null;
player = null
continue;
continue
}
}
break;
break
}
}
}
}
Line 466: Line 464:
{
{
if (!fallback)
if (!fallback)
return null;
return null
player = fallback;
player = fallback
}
}
// to detect the rune that was spawned, every existing rune must be hidden
// to detect the rune that was spawned, every existing rune must be hidden
for (local rune; rune = Entities.FindByClassname(rune, "item_powerup_rune");)
for (local rune; rune = Entities.FindByClassname(rune, "item_powerup_rune");)
rune.KeyValueFromString("classname", "zitem_powerup_rune");
rune.KeyValueFromString("classname", "zitem_powerup_rune")
local cond = RuneTypeToCond[type];
local cond = RuneTypeToCond[type]
// if player already has a rune, temporarily strip it
// if player already has a rune, temporarily strip it
local player_cond, player_cond_duration;
local player_cond, player_cond_duration
if (player.IsCarryingRune())
if (player.IsCarryingRune())
{
{
foreach (cond in RuneTypeToCond)
foreach (cond in RuneTypeToCond)
{
{
player_cond_duration = player.GetCondDuration(cond);
player_cond_duration = player.GetCondDuration(cond)
if (player_cond_duration != 0.0)
if (player_cond_duration != 0.0)
{
{
player.RemoveCond(cond);
player.RemoveCond(cond)
player_cond = cond;
player_cond = cond
break;
break
}
}
}
}
}
}


local cond_prop = "m_Shared." + (cond >= 96 ? "m_nPlayerCondEx3" : "m_nPlayerCondEx2");
local cond_prop = "m_Shared." + (cond >= 96 ? "m_nPlayerCondEx3" : "m_nPlayerCondEx2")
local cond_bits = NetProps.GetPropInt(player, cond_prop);
local cond_bits = NetProps.GetPropInt(player, cond_prop)
NetProps.SetPropInt(player, cond_prop, cond_bits | (1 << (cond % 32)));
NetProps.SetPropInt(player, cond_prop, cond_bits | (1 << (cond % 32)))
player.DropRune(false, team);
player.DropRune(false, team)
NetProps.SetPropInt(player, cond_prop, cond_bits);
NetProps.SetPropInt(player, cond_prop, cond_bits)
// give original rune back
// give original rune back
if (player_cond)
if (player_cond)
player.AddCondEx(player_cond, player_cond_duration, null);
player.AddCondEx(player_cond, player_cond_duration, null)
local rune = Entities.FindByClassname(null, "item_powerup_rune");
local rune = Entities.FindByClassname(null, "item_powerup_rune")
if (!rune)
if (!rune)
return null;
return null
rune.Teleport(true, origin, true, angles, true, velocity);
rune.Teleport(true, origin, true, angles, true, velocity)
if (!reposition)
if (!reposition)
Line 512: Line 510:
// prevents rune from blinking after 30 or 60 seconds
// prevents rune from blinking after 30 or 60 seconds
// and teleporting to a spawnpoint if one exists
// and teleporting to a spawnpoint if one exists
rune.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION);
rune.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION)
rune.AddSolidFlags(Constants.FSolid.FSOLID_TRIGGER);
rune.AddSolidFlags(Constants.FSolid.FSOLID_TRIGGER)
}
}
return rune;
return rune
}
}
</source>
</source>
Line 527: Line 525:
{
{
// spawn a fake dragon's fury to emit the fireballs
// spawn a fake dragon's fury to emit the fireballs
FireballMaker <- Entities.CreateByClassname("tf_weapon_rocketlauncher_fireball");
FireballMaker <- Entities.CreateByClassname("tf_weapon_rocketlauncher_fireball")
NetProps.SetPropInt(FireballMaker, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 1178);
NetProps.SetPropInt(FireballMaker, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 1178)
NetProps.SetPropBool(FireballMaker, "m_AttributeManager.m_Item.m_bInitialized", true);
NetProps.SetPropBool(FireballMaker, "m_AttributeManager.m_Item.m_bInitialized", true)
Entities.DispatchSpawn(FireballMaker);
FireballMaker.DispatchSpawn()
FireballMaker.SetClip1(-1);
FireballMaker.SetClip1(-1)
}
}


function CheckMeleeSmack()
function CheckMeleeSmack()
{
{
local owner = self.GetOwner();
local owner = self.GetOwner()
// when melee smacks, m_iNextMeleeCrit is 0
// when melee smacks, m_iNextMeleeCrit is 0
if (NetProps.GetPropInt(owner, "m_Shared.m_iNextMeleeCrit") == 0)
if (NetProps.GetPropInt(owner, "m_Shared.m_iNextMeleeCrit") == 0)
Line 544: Line 542:
{
{
// preserve old charge meter and ammo count
// preserve old charge meter and ammo count
local charge = NetProps.GetPropFloat(owner, "m_Shared.m_flItemChargeMeter");
local charge = NetProps.GetPropFloat(owner, "m_Shared.m_flItemChargeMeter")
local ammo = NetProps.GetPropIntArray(owner, "m_iAmmo", 1);
local ammo = NetProps.GetPropIntArray(owner, "m_iAmmo", 1)
// set up stuff needed to ensure the weapon always fires
// set up stuff needed to ensure the weapon always fires
NetProps.SetPropIntArray(owner, "m_iAmmo", 99, 1);
NetProps.SetPropIntArray(owner, "m_iAmmo", 99, 1)
NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", 100.0);
NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", 100.0)
NetProps.SetPropBool(owner, "m_bLagCompensation", false);
NetProps.SetPropBool(owner, "m_bLagCompensation", false)
NetProps.SetPropFloat(FireballMaker, "m_flNextPrimaryAttack", 0);
NetProps.SetPropFloat(FireballMaker, "m_flNextPrimaryAttack", 0)
NetProps.SetPropEntity(FireballMaker, "m_hOwner", owner);
NetProps.SetPropEntity(FireballMaker, "m_hOwner", owner)


FireballMaker.PrimaryAttack();
FireballMaker.PrimaryAttack()
// revert changes
// revert changes
NetProps.SetPropBool(owner, "m_bLagCompensation", true);
NetProps.SetPropBool(owner, "m_bLagCompensation", true)
NetProps.SetPropIntArray(owner, "m_iAmmo", ammo, 1);
NetProps.SetPropIntArray(owner, "m_iAmmo", ammo, 1)
NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", charge);
NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", charge)
}
}
// continue smack detection
// continue smack detection
NetProps.SetPropInt(owner, "m_Shared.m_iNextMeleeCrit", -2);
NetProps.SetPropInt(owner, "m_Shared.m_iNextMeleeCrit", -2)
}
}
return -1;
return -1
}
}


ClearGameEventCallbacks();
ClearGameEventCallbacks()


function OnGameEvent_player_spawn(params)
function OnGameEvent_player_spawn(params)
{
{
local player = GetPlayerFromUserID(params.userid);
local player = GetPlayerFromUserID(params.userid)
if (!player)
if (!player)
return;
return
local player_class = params["class"];
local player_class = params["class"]
if (player_class != Constants.ETFClass.TF_CLASS_HEAVYWEAPONS)
if (player_class != Constants.ETFClass.TF_CLASS_HEAVYWEAPONS)
return;
return
for (local i = 0; i < 8; i++)
for (local i = 0; i < 8; i++)
{
{
local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
if (weapon == null || !weapon.IsMeleeWeapon())
if (weapon == null || !weapon.IsMeleeWeapon())
continue;
continue
// this kicks off melee smack detection logic
// this kicks off melee smack detection logic
NetProps.SetPropInt(player, "m_Shared.m_iNextMeleeCrit", -2);
NetProps.SetPropInt(player, "m_Shared.m_iNextMeleeCrit", -2)
AddThinkToEnt(weapon, "CheckMeleeSmack");
AddThinkToEnt(weapon, "CheckMeleeSmack")
break;
break
}
}
}
}


__CollectGameEventCallbacks(this);
__CollectGameEventCallbacks(this)
</source>
</source>


Line 603: Line 601:
function CheckWeaponFire()
function CheckWeaponFire()
{
{
local fire_time = NetProps.GetPropFloat(self, "m_flLastFireTime");
local fire_time = NetProps.GetPropFloat(self, "m_flLastFireTime")
if (fire_time > last_fire_time)
if (fire_time > last_fire_time)
{
{
printf("%f %s : Fired\n", Time(), self.GetClassname());
printf("%f %s : Fired\n", Time(), self.GetClassname())
local owner = self.GetOwner();
local owner = self.GetOwner()
if (owner)
if (owner)
{
{
owner.SetAbsVelocity(owner.GetAbsVelocity() - owner.EyeAngles().Forward() * 800.0);
owner.SetAbsVelocity(owner.GetAbsVelocity() - owner.EyeAngles().Forward() * 800.0)
}
}
last_fire_time = fire_time;
last_fire_time = fire_time
}
}
return -1;
return -1
}
}


ClearGameEventCallbacks();
ClearGameEventCallbacks()


function OnGameEvent_player_spawn(params)
function OnGameEvent_player_spawn(params)
{
{
local player = GetPlayerFromUserID(params.userid);
local player = GetPlayerFromUserID(params.userid)
if (!player)
if (!player)
return;
return
for (local i = 0; i < 8; i++)
for (local i = 0; i < 8; i++)
{
{
local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
if (weapon == null || weapon.IsMeleeWeapon())
if (weapon == null || weapon.IsMeleeWeapon())
continue;
continue
weapon.ValidateScriptScope();
weapon.ValidateScriptScope()
weapon.GetScriptScope().last_fire_time <- 0.0;
weapon.GetScriptScope().last_fire_time <- 0.0
weapon.GetScriptScope().CheckWeaponFire <- CheckWeaponFire;
weapon.GetScriptScope().CheckWeaponFire <- CheckWeaponFire
AddThinkToEnt(weapon, "CheckWeaponFire");
AddThinkToEnt(weapon, "CheckWeaponFire")
}
}
}
}


__CollectGameEventCallbacks(this);
__CollectGameEventCallbacks(this)
</source>
</source>


Line 650: Line 648:
     if (params.patient == params.healer)
     if (params.patient == params.healer)
     {
     {
         local player = GetPlayerFromUserID(params.patient);
         local player = GetPlayerFromUserID(params.patient)
         if (player  
         if (player  
             && player.GetPlayerClass() == Constants.ETFClass.TF_CLASS_MEDIC
             && player.GetPlayerClass() == Constants.ETFClass.TF_CLASS_MEDIC
             && NetProps.GetPropInt(player, "m_bitsDamageType") == 32)
             && NetProps.GetPropInt(player, "m_bitsDamageType") == 32)
         {
         {
             player.SetHealth(player.GetHealth() - params.amount);
             player.SetHealth(player.GetHealth() - params.amount)
         }
         }
     }
     }
Line 681: Line 679:
if (!("disintegrate_proxy_weapon" in getroottable()))
if (!("disintegrate_proxy_weapon" in getroottable()))
{
{
     ::disintegrate_proxy_weapon <- null;
     ::disintegrate_proxy_weapon <- null
     ::disintegrate_immune_conds <-  
     ::disintegrate_immune_conds <-  
Line 690: Line 688:
         Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
         Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
         Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
         Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
     ];
     ]
}
}


Line 696: Line 694:
if (!disintegrate_proxy_weapon || !disintegrate_proxy_weapon.IsValid())
if (!disintegrate_proxy_weapon || !disintegrate_proxy_weapon.IsValid())
{
{
     disintegrate_proxy_weapon = Entities.CreateByClassname("tf_weapon_bat");
     disintegrate_proxy_weapon = Entities.CreateByClassname("tf_weapon_bat")
     NetProps.SetPropInt(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 349);
     NetProps.SetPropInt(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 349)
     NetProps.SetPropBool(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true);
     NetProps.SetPropBool(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
     disintegrate_proxy_weapon.DispatchSpawn();
     disintegrate_proxy_weapon.DispatchSpawn()
disintegrate_proxy_weapon.DisableDraw();
disintegrate_proxy_weapon.DisableDraw()
// Add the attribute that creates disintegration
// Add the attribute that creates disintegration
     disintegrate_proxy_weapon.AddAttribute("ragdolls become ash", 1.0, -1.0);
     disintegrate_proxy_weapon.AddAttribute("ragdolls become ash", 1.0, -1.0)
}
}


Line 709: Line 707:
{
{
// Add an output to deal damage when the trigger is touched
// Add an output to deal damage when the trigger is touched
     self.ConnectOutput("OnStartTouch", "Disintegrate");
     self.ConnectOutput("OnStartTouch", "Disintegrate")
}
}


Line 716: Line 714:
// Remove conditions that give immunity to damage
// Remove conditions that give immunity to damage
     foreach (cond in disintegrate_immune_conds)
     foreach (cond in disintegrate_immune_conds)
         activator.RemoveCondEx(cond, true);
         activator.RemoveCondEx(cond, true)
          
          
// Set any owner on the weapon to prevent a crash
// Set any owner on the weapon to prevent a crash
     NetProps.SetPropEntity(disintegrate_proxy_weapon, "m_hOwner", activator);
     NetProps.SetPropEntity(disintegrate_proxy_weapon, "m_hOwner", activator)
// Deal the damage with the weapon
// Deal the damage with the weapon
     activator.TakeDamageCustom(self, activator, disintegrate_proxy_weapon,  
     activator.TakeDamageCustom(self, activator, disintegrate_proxy_weapon,  
                                 Vector(0,0,0), Vector(0,0,0),  
                                 Vector(0,0,0), Vector(0,0,0),  
                                 99999.0, 2080, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BURNING);
                                 99999.0, 2080, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BURNING)
}
}
</source>
</source>
Line 734: Line 732:
if (!("freeze_proxy_weapon" in getroottable()))
if (!("freeze_proxy_weapon" in getroottable()))
{
{
     ::freeze_proxy_weapon <- null;
     ::freeze_proxy_weapon <- null
     ::freeze_immune_conds <-  
     ::freeze_immune_conds <-  
Line 743: Line 741:
         Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
         Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
         Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
         Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
     ];
     ]
}
}


Line 749: Line 747:
if (!freeze_proxy_weapon || !freeze_proxy_weapon.IsValid())
if (!freeze_proxy_weapon || !freeze_proxy_weapon.IsValid())
{
{
     freeze_proxy_weapon = Entities.CreateByClassname("tf_weapon_knife");
     freeze_proxy_weapon = Entities.CreateByClassname("tf_weapon_knife")
     NetProps.SetPropInt(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 649);
     NetProps.SetPropInt(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 649)
     NetProps.SetPropBool(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true);
     NetProps.SetPropBool(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
     freeze_proxy_weapon.DispatchSpawn();
     freeze_proxy_weapon.DispatchSpawn()
freeze_proxy_weapon.DisableDraw();
freeze_proxy_weapon.DisableDraw()
// Add the attribute that creates ice statues
// Add the attribute that creates ice statues
freeze_proxy_weapon.AddAttribute("freeze backstab victim", 1.0, -1.0);
freeze_proxy_weapon.AddAttribute("freeze backstab victim", 1.0, -1.0)
}
}


Line 762: Line 760:
{
{
// Add an output to deal damage when the trigger is touched
// Add an output to deal damage when the trigger is touched
     self.ConnectOutput("OnStartTouch", "Freeze");
     self.ConnectOutput("OnStartTouch", "Freeze")
}
}


Line 769: Line 767:
// Remove conditions that give immunity to damage
// Remove conditions that give immunity to damage
     foreach (cond in freeze_immune_conds)
     foreach (cond in freeze_immune_conds)
         activator.RemoveCondEx(cond, true);
         activator.RemoveCondEx(cond, true)
          
          
// Set any owner on the weapon to prevent a crash
// Set any owner on the weapon to prevent a crash
     NetProps.SetPropEntity(freeze_proxy_weapon, "m_hOwner", activator);
     NetProps.SetPropEntity(freeze_proxy_weapon, "m_hOwner", activator)
// Deal the damage with the weapon
// Deal the damage with the weapon
     activator.TakeDamageCustom(self, activator, freeze_proxy_weapon,  
     activator.TakeDamageCustom(self, activator, freeze_proxy_weapon,  
                                 Vector(0,0,0), Vector(0,0,0),  
                                 Vector(0,0,0), Vector(0,0,0),  
                                 99999.0, Constants.FDmgType.DMG_CLUB, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BACKSTAB);
                                 99999.0, Constants.FDmgType.DMG_CLUB, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BACKSTAB)
// I don't remember why this is needed but it's important
// I don't remember why this is needed but it's important
local ragdoll = NetProps.GetPropEntity(activator, "m_hRagdoll");
local ragdoll = NetProps.GetPropEntity(activator, "m_hRagdoll")
if (ragdoll)
if (ragdoll)
NetProps.SetPropInt(ragdoll, "m_iDamageCustom", 0);
NetProps.SetPropInt(ragdoll, "m_iDamageCustom", 0)
}
}
</source>
</source>
Line 802: Line 800:
     {
     {
         ClientPrint(player, 3, text)
         ClientPrint(player, 3, text)
         return;
         return
     }
     }
      
      
Line 811: Line 809:
     for (local i = splittext.len() - 1; i >= 0; i--)
     for (local i = splittext.len() - 1; i >= 0; i--)
         if (splittext[i].len() < 1)  
         if (splittext[i].len() < 1)  
             splittext.remove(i);
             splittext.remove(i)
      
      
     //format into new string
     //format into new string
Line 828: Line 826:


<source lang=js>
<source lang=js>
::PlayerManager <- Entities.FindByClassname(null, "tf_player_manager");
::PlayerManager <- Entities.FindByClassname(null, "tf_player_manager")
::GetPlayerUserID <- function(player)
::GetPlayerUserID <- function(player)
{
{
     return NetProps.GetPropIntArray(PlayerManager, "m_iUserID", player.entindex());
     return NetProps.GetPropIntArray(PlayerManager, "m_iUserID", player.entindex())
}
}
</source>
</source>
Line 838: Line 836:
{{tf2|2}} has a subdued physics mode by default, which makes players poorly interact with physics objects unlike other Source games. This can be fixed by setting the <code>sv_turbophysics</code> [[Team_Fortress_2/Script_Functions#Convars|convar]] to 0.
{{tf2|2}} has a subdued physics mode by default, which makes players poorly interact with physics objects unlike other Source games. This can be fixed by setting the <code>sv_turbophysics</code> [[Team_Fortress_2/Script_Functions#Convars|convar]] to 0.
<source lang=js>
<source lang=js>
Convars.SetValue("sv_turbophysics", 0);
Convars.SetValue("sv_turbophysics", 0)
</source>
</source>


[[func_tracktrain]]s have similar problems. The game has a special train physics mode for Payload to circumvent this issue, which is normally not enabled in other gamemodes. This can be forcefully enabled (or disabled!) via the following:
[[func_tracktrain]]s have similar problems. The game has a special train physics mode for Payload to circumvent this issue, which is normally not enabled in other gamemodes. This can be forcefully enabled (or disabled!) via the following:
<source lang=js>
<source lang=js>
ForceEscortPushLogic(2);
ForceEscortPushLogic(2)
</source>
</source>


Line 850: Line 848:


<source lang=js>
<source lang=js>
NetProps.SetPropInt(player, "m_iObserverLastMode", 5);
NetProps.SetPropInt(player, "m_iObserverLastMode", 5)
local team = player.GetTeam();
local team = player.GetTeam()
NetProps.SetPropInt(player, "m_iTeamNum", 1);
NetProps.SetPropInt(player, "m_iTeamNum", 1)
player.DispatchSpawn();
player.DispatchSpawn()
NetProps.SetPropInt(player, "m_iTeamNum", team);
NetProps.SetPropInt(player, "m_iTeamNum", team)
</source>
</source>


Line 917: Line 915:
<source lang=js>
<source lang=js>
::ForceChangeClass <- function(player, classIndex) {
::ForceChangeClass <- function(player, classIndex) {
player.SetPlayerClass(classIndex);
player.SetPlayerClass(classIndex)
NetProps.SetPropInt(player, "m_Shared.m_iDesiredPlayerClass", classIndex);
NetProps.SetPropInt(player, "m_Shared.m_iDesiredPlayerClass", classIndex)
player.ForceRegenerateAndRespawn();
player.ForceRegenerateAndRespawn()
}
}
ForceChangeClass(GetListenServerHost(), Constants.ETFClass.TF_CLASS_SCOUT);
ForceChangeClass(GetListenServerHost(), Constants.ETFClass.TF_CLASS_SCOUT)
</source>
</source>


Line 933: Line 931:
if (params.full_reset) {
if (params.full_reset) {
for (local ent = null; ent = Entities.FindByClassname(ent, "trigger_capture_area");) {
for (local ent = null; ent = Entities.FindByClassname(ent, "trigger_capture_area");) {
local capPointName = NetProps.GetPropString(ent, "m_iszCapPointName");
local capPointName = NetProps.GetPropString(ent, "m_iszCapPointName")
local capTimeInSeconds = NetProps.GetPropFloat(ent, "m_flCapTime");
local capTimeInSeconds = NetProps.GetPropFloat(ent, "m_flCapTime")
// Double the time it takes to capture a control point
// Double the time it takes to capture a control point
capTimeInSeconds *= 2;
capTimeInSeconds *= 2
NetProps.SetPropFloat(ent, "m_flCapTime", capTimeInSeconds);
NetProps.SetPropFloat(ent, "m_flCapTime", capTimeInSeconds)
// If we do not fire this event, then cap time total does not propagate to the client
// If we do not fire this event, then cap time total does not propagate to the client
// and the UI will be buggy and show the control point capping quicker then it should
// and the UI will be buggy and show the control point capping quicker then it should
// and then "popping" back after the server updates the client.
// and then "popping" back after the server updates the client.
EntFireByHandle(ent, "SetControlPoint", capPointName, 0.0, null, null);
EntFireByHandle(ent, "SetControlPoint", capPointName, 0.0, null, null)
}
}
}
}
Line 951: Line 949:


<source lang=js>
<source lang=js>
local proxy_entity = Entities.CreateByClassname("obj_teleporter"); // not using SpawnEntityFromTable as that creates spawning noises
local proxy_entity = Entities.CreateByClassname("obj_teleporter") // not using SpawnEntityFromTable as that creates spawning noises
proxy_entity.SetAbsOrigin(Vector(0, -320, -150));
proxy_entity.SetAbsOrigin(Vector(0, -320, -150))
proxy_entity.DispatchSpawn();
proxy_entity.DispatchSpawn()
proxy_entity.SetModel("models/player/heavy.mdl");
proxy_entity.SetModel("models/player/heavy.mdl")
proxy_entity.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION) // prevents the entity from disappearing
proxy_entity.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION) // prevents the entity from disappearing
proxy_entity.SetSolid(Constants.ESolidType.SOLID_NONE);
proxy_entity.SetSolid(Constants.ESolidType.SOLID_NONE)
NetProps.SetPropBool(proxy_entity, "m_bPlacing", true);
NetProps.SetPropBool(proxy_entity, "m_bPlacing", true)
NetProps.SetPropInt(proxy_entity, "m_fObjectFlags", 2); // sets "attachment" flag, prevents entity being snapped to player feet
NetProps.SetPropInt(proxy_entity, "m_fObjectFlags", 2) // sets "attachment" flag, prevents entity being snapped to player feet


// m_hBuilder is the player who the entity will be networked to only
// m_hBuilder is the player who the entity will be networked to only
NetProps.SetPropEntity(proxy_entity, "m_hBuilder", GetListenServerHost());
NetProps.SetPropEntity(proxy_entity, "m_hBuilder", GetListenServerHost())
</source>
</source>


Line 968: Line 966:
The example below demonstrates this to detect if a point is inside a respawn room.
The example below demonstrates this to detect if a point is inside a respawn room.


Example usage: <code>printl(IsPointInRespawnRoom(GetListenServerHost().EyePosition()));</code>
Example usage: <code>printl(IsPointInRespawnRoom(GetListenServerHost().EyePosition()))</code>
{{note|The <code>SetCollisionGroup</code> lines are only necessary for <code>func_respawnroom</code>. Other triggers don't need those lines.}}
{{note|The <code>SetCollisionGroup</code> lines are only necessary for <code>func_respawnroom</code>. Other triggers don't need those lines.}}


Line 974: Line 972:
function IsPointInRespawnRoom(point)
function IsPointInRespawnRoom(point)
{
{
local triggers = [];
local triggers = []
for (local trigger; trigger = Entities.FindByClassname(trigger, "func_respawnroom");)
for (local trigger; trigger = Entities.FindByClassname(trigger, "func_respawnroom");)
{
{
trigger.SetCollisionGroup(0);
trigger.SetCollisionGroup(0)
trigger.RemoveSolidFlags(4); // FSOLID_NOT_SOLID
trigger.RemoveSolidFlags(4) // FSOLID_NOT_SOLID
triggers.append(trigger);
triggers.append(trigger)
}
}
Line 988: Line 986:
mask = 0
mask = 0
}
}
TraceLineEx(trace);
TraceLineEx(trace)
foreach (trigger in triggers)
foreach (trigger in triggers)
{
{
trigger.SetCollisionGroup(25); // special collision group used by respawnrooms only
trigger.SetCollisionGroup(25) // special collision group used by respawnrooms only
trigger.AddSolidFlags(4); // FSOLID_NOT_SOLID
trigger.AddSolidFlags(4) // FSOLID_NOT_SOLID
}
}


return trace.hit && trace.enthit.GetClassname() == "func_respawnroom";
return trace.hit && trace.enthit.GetClassname() == "func_respawnroom"
}
}
</source>
</source>
Line 1,020: Line 1,018:
{
{
     local gamerules = Entities.FindByClassname(null, "tf_gamerules")
     local gamerules = Entities.FindByClassname(null, "tf_gamerules")
     NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", false);
     NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", false)
     player.ForceChangeTeam(teamnum, false);
     player.ForceChangeTeam(teamnum, false)
     NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", true);
     NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", true)
}
}
ChangePlayerTeamMvM(GetListenServerHost(), Constants.ETFTeam.TF_TEAM_PVE_INVADERS) //Constants.ETFTeam.TF_TEAM_BLUE is also perfectly valid
ChangePlayerTeamMvM(GetListenServerHost(), Constants.ETFTeam.TF_TEAM_PVE_INVADERS) //Constants.ETFTeam.TF_TEAM_BLUE is also perfectly valid
Line 1,032: Line 1,030:


<source lang=js>
<source lang=js>
local resource = Entities.FindByClassname(null, "tf_objective_resource");
local resource = Entities.FindByClassname(null, "tf_objective_resource")
NetProps.SetPropString(resource, "m_iszMvMPopfileName", "New Mission Name Here");
NetProps.SetPropString(resource, "m_iszMvMPopfileName", "New Mission Name Here")
</source>
</source>


Line 1,042: Line 1,040:
function ChangeLevel()
function ChangeLevel()
{
{
local intermission = SpawnEntityFromTable("point_intermission", {});
local intermission = SpawnEntityFromTable("point_intermission", {})
Convars.SetValue("mp_chattime", 0);
Convars.SetValue("mp_chattime", 0)
EntFireByHandle(intermission, "Activate", "", 0, null, null);
EntFireByHandle(intermission, "Activate", "", 0, null, null)
}
}
</source>
</source>
Line 1,056: Line 1,054:
     // printf("Soundscape Index: %d \t", GetPropInt(player, "m_Local.m_audio.soundscapeIndex")) //uncomment to print soundscape indexes to console, enable "developer 1" to match with the name of the soundscape
     // printf("Soundscape Index: %d \t", GetPropInt(player, "m_Local.m_audio.soundscapeIndex")) //uncomment to print soundscape indexes to console, enable "developer 1" to match with the name of the soundscape
if (GetPropInt(player, "m_Local.m_audio.soundscapeIndex") != index)
if (GetPropInt(player, "m_Local.m_audio.soundscapeIndex") != index)
return false;
return false
else
else
return true;
return true
}
}


Line 1,064: Line 1,062:
for (local soundscape; soundscape = Entities.FindByClassname(soundscape, "env_soundscape*"); )
for (local soundscape; soundscape = Entities.FindByClassname(soundscape, "env_soundscape*"); )
{
{
EntityOutputs.AddOutput(soundscape, "OnPlay", "!self", "RunScriptCode", "printl(InSoundscapeIndex(GetListenServerHost(), 34))", 0, -1);
EntityOutputs.AddOutput(soundscape, "OnPlay", "!self", "RunScriptCode", "printl(InSoundscapeIndex(GetListenServerHost(), 34))", 0, -1)
}
}
</source>
</source>
Line 1,078: Line 1,076:
for (local scene; scene = Entities.FindByClassname(scene, "instanced_scripted_scene");)
for (local scene; scene = Entities.FindByClassname(scene, "instanced_scripted_scene");)
{
{
local owner = GetPropEntity(scene, "m_hOwner");
local owner = GetPropEntity(scene, "m_hOwner")
if (owner == self)
if (owner == self)
{
{
local name = GetPropString(scene, "m_szInstanceFilename");
local name = GetPropString(scene, "m_szInstanceFilename")
local thriller_name = self.GetPlayerClass() == TF_CLASS_MEDIC ? "taunt07" : "taunt06";
local thriller_name = self.GetPlayerClass() == TF_CLASS_MEDIC ? "taunt07" : "taunt06"
if (name.find(thriller_name) != null)
if (name.find(thriller_name) != null)
{
{
scene.Kill();
scene.Kill()
self.RemoveCond(TF_COND_TAUNTING);
self.RemoveCond(TF_COND_TAUNTING)
self.Taunt(TAUNT_BASE_WEAPON, 0);
self.Taunt(TAUNT_BASE_WEAPON, 0)
break;
break
}
}
}
}
Line 1,094: Line 1,092:
}
}
return -1;
return -1
}
}
</source>
</source>
Line 1,108: Line 1,106:
NetProps.SetPropBool(wearable, "m_AttributeManager.m_Item.m_bInitialized", true)
NetProps.SetPropBool(wearable, "m_AttributeManager.m_Item.m_bInitialized", true)
wearable.SetAbsOrigin(origin)
wearable.SetAbsOrigin(origin)
wearable.AddAttribute("set item tint RGB", 15132390, -1);
wearable.AddAttribute("set item tint RGB", 15132390, -1)
wearable.DispatchSpawn()
wearable.DispatchSpawn()
wearable.EnableDraw()
wearable.EnableDraw()
Line 1,164: Line 1,162:


<source lang=js>
<source lang=js>
local GameRules = Entities.FindByClassname(null, "tf_gamerules");
local GameRules = Entities.FindByClassname(null, "tf_gamerules")
local MAX_CLIENTS = MaxClients().tointeger();
local MAX_CLIENTS = MaxClients().tointeger()


// 0 - not ready
// 0 - not ready
// 1 - ready
// 1 - ready
local ready_state = 1;
local ready_state = 1


local red_check = false;
local red_check = false
local blue_check = false;
local blue_check = false


for (local i = 1; i <= MAX_CLIENTS; i++)
for (local i = 1; i <= MAX_CLIENTS; i++)
{
{
local player = PlayerInstanceFromIndex(i);
local player = PlayerInstanceFromIndex(i)
if (!player)
if (!player)
continue;
continue
local team = player.GetTeam();
local team = player.GetTeam()
if (!(team & 2))
if (!(team & 2))
continue;
continue
if (team == 2)
if (team == 2)
{
{
if (red_check)
if (red_check)
continue;
continue
red_check = true;
red_check = true
}
}
else if (team == 3)
else if (team == 3)
{
{
if (blue_check)
if (blue_check)
continue;
continue
blue_check = true;
blue_check = true
}
}
if (NetProps.GetPropBoolArray(GameRules, "m_bTeamReady", team))
if (NetProps.GetPropBoolArray(GameRules, "m_bTeamReady", team))
continue;
continue
NetProps.SetPropBoolArray(GameRules, "m_bTeamReady", ready_state == 1, team);
NetProps.SetPropBoolArray(GameRules, "m_bTeamReady", ready_state == 1, team)
SendGlobalGameEvent("tournament_stateupdate",
SendGlobalGameEvent("tournament_stateupdate",
Line 1,210: Line 1,208:
newname = " ",
newname = " ",
});
})


if (ready_state == 0)
if (ready_state == 0)
{
{
NetProps.SetPropFloat(GameRules, "m_flRestartRoundTime", -1.0);
NetProps.SetPropFloat(GameRules, "m_flRestartRoundTime", -1.0)
NetProps.SetPropBool(GameRules, "m_bAwaitingReadyRestart", true);
NetProps.SetPropBool(GameRules, "m_bAwaitingReadyRestart", true)
}
}
}
}
Line 1,226: Line 1,224:
{{todo|The weapon is holstered each time, fixing this requires manually spoofing <code>Weapon_Switch</code>.}}
{{todo|The weapon is holstered each time, fixing this requires manually spoofing <code>Weapon_Switch</code>.}}
<source lang=js>
<source lang=js>
const MAX_WEAPONS = 8;
const MAX_WEAPONS = 8


function SapBuilding(building)
function SapBuilding(building)
{
{
// select first player found
// select first player found
local player = Entities.FindByClassname(null, "player");
local player = Entities.FindByClassname(null, "player")
if (!player)
if (!player)
return;
return
// find existing builder weapon if applicable
// find existing builder weapon if applicable
local player_class = player.GetPlayerClass();
local player_class = player.GetPlayerClass()
local old_builder, old_slot;
local old_builder, old_slot
if (player_class == 8 || player_class == 9) // TF_CLASS_SPY or TF_CLASS_ENGINEER
if (player_class == 8 || player_class == 9) // TF_CLASS_SPY or TF_CLASS_ENGINEER
{
{
for (local i = 0; i < MAX_WEAPONS; i++)
for (local i = 0; i < MAX_WEAPONS; i++)
{
{
local held_weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
local held_weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
if (held_weapon == null)
if (held_weapon == null)
continue;
continue
if (held_weapon.GetClassname() != "tf_weapon_builder")
if (held_weapon.GetClassname() != "tf_weapon_builder")
continue;
continue


old_builder = held_weapon;
old_builder = held_weapon
old_slot = i;
old_slot = i
NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i);
NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i)
break;
break
}
}
}
}
local old_lifestate = NetProps.GetPropInt(player, "m_lifeState");
local old_lifestate = NetProps.GetPropInt(player, "m_lifeState")
local old_team = player.GetTeam();
local old_team = player.GetTeam()
local building_team = building.GetTeam();
local building_team = building.GetTeam()
local enemy_team;
local enemy_team
if (building_team == 2) // red
if (building_team == 2) // red
enemy_team = 3;
enemy_team = 3
else if (building_team == 3) // blue
else if (building_team == 3) // blue
enemy_team = 2;
enemy_team = 2
else
else
enemy_team = 2;
enemy_team = 2
// spoof being alive and on opposite team
// spoof being alive and on opposite team
NetProps.SetPropInt(player, "m_lifeState", 0);
NetProps.SetPropInt(player, "m_lifeState", 0)
NetProps.SetPropInt(player, "m_iTeamNum", enemy_team);
NetProps.SetPropInt(player, "m_iTeamNum", enemy_team)
// give sapper weapon
// give sapper weapon
     local weapon = Entities.CreateByClassname("tf_weapon_builder");
     local weapon = Entities.CreateByClassname("tf_weapon_builder")
     NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 735);
     NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 735)
     NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true);
     NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
     NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true);
     NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true)
     weapon.SetTeam(enemy_team);
     weapon.SetTeam(enemy_team)
     Entities.DispatchSpawn(weapon);
     weapon.DispatchSpawn()
NetProps.SetPropInt(weapon, "m_iObjectType", 3);
NetProps.SetPropInt(weapon, "m_iObjectType", 3)
NetProps.SetPropInt(weapon, "m_iSubType", 3);
NetProps.SetPropInt(weapon, "m_iSubType", 3)
NetProps.SetPropIntArray(weapon, "m_aBuildableObjectTypes", 1, 3);
NetProps.SetPropIntArray(weapon, "m_aBuildableObjectTypes", 1, 3)
player.Weapon_Equip(weapon);
player.Weapon_Equip(weapon)


// teleport player to the building and hold the sapper
// teleport player to the building and hold the sapper
local old_origin = player.GetOrigin();
local old_origin = player.GetOrigin()
local old_angles = player.LocalEyeAngles();
local old_angles = player.LocalEyeAngles()
local old_weapon = player.GetActiveWeapon();
local old_weapon = player.GetActiveWeapon()
player.SetAbsOrigin(building.GetOrigin() - Vector(64, 0, 0));
player.SetAbsOrigin(building.GetOrigin() - Vector(64, 0, 0))
NetProps.SetPropVector(player, "pl.v_angle", Vector());
NetProps.SetPropVector(player, "pl.v_angle", Vector())
player.Weapon_Switch(weapon);
player.Weapon_Switch(weapon)
weapon.PrimaryAttack();
weapon.PrimaryAttack()
// remove owner from the sapper that was just placed
// remove owner from the sapper that was just placed
Line 1,300: Line 1,298:
if (NetProps.GetPropEntity(sapper, "m_hBuiltOnEntity") == building)
if (NetProps.GetPropEntity(sapper, "m_hBuiltOnEntity") == building)
{
{
NetProps.SetPropEntity(sapper, "m_hBuilder", null);
NetProps.SetPropEntity(sapper, "m_hBuilder", null)
break;
break
}
}
}
}
Line 1,307: Line 1,305:
// revert all changes
// revert all changes
if (old_builder)
if (old_builder)
NetProps.SetPropEntityArray(player, "m_hMyWeapons", old_builder, old_slot);
NetProps.SetPropEntityArray(player, "m_hMyWeapons", old_builder, old_slot)
NetProps.SetPropInt(player, "m_lifeState", old_lifestate);
NetProps.SetPropInt(player, "m_lifeState", old_lifestate)
NetProps.SetPropInt(player, "m_iTeamNum", old_team);
NetProps.SetPropInt(player, "m_iTeamNum", old_team)
player.Weapon_Switch(old_weapon);
player.Weapon_Switch(old_weapon)
NetProps.SetPropVector(player, "pl.v_angle", old_angles + Vector());
NetProps.SetPropVector(player, "pl.v_angle", old_angles + Vector())
player.SetAbsOrigin(old_origin);
player.SetAbsOrigin(old_origin)
weapon.Destroy();
weapon.Destroy()
}
}


// example
// example
local building = Entities.FindByClassname(null, "obj_dispenser");
local building = Entities.FindByClassname(null, "obj_dispenser")
if (building)
if (building)
{
{
if (!NetProps.GetPropBool(building, "m_bHasSapper"))
if (!NetProps.GetPropBool(building, "m_bHasSapper"))
SapBuilding(building);
SapBuilding(building)
}
}
</source>
</source>
Line 1,342: Line 1,340:
function NormalizeAngle(target)
function NormalizeAngle(target)
{
{
target %= 360.0;
target %= 360.0
if (target > 180.0)
if (target > 180.0)
target -= 360.0;
target -= 360.0
else if (target < -180.0)
else if (target < -180.0)
target += 360.0;
target += 360.0
return target;
return target
}
}


Line 1,353: Line 1,351:
function ApproachAngle(target, value, speed)
function ApproachAngle(target, value, speed)
{
{
target = NormalizeAngle(target);
target = NormalizeAngle(target)
value = NormalizeAngle(value);
value = NormalizeAngle(value)
local delta = NormalizeAngle(target - value);
local delta = NormalizeAngle(target - value)
if (delta > speed)
if (delta > speed)
return value + speed;
return value + speed
else if (delta < -speed)
else if (delta < -speed)
return value - speed;
return value - speed
return value;
return value
}
}


Line 1,366: Line 1,364:
function VectorAngles(forward)
function VectorAngles(forward)
{
{
local yaw, pitch;
local yaw, pitch
if ( forward.y == 0.0 && forward.x == 0.0 )
if ( forward.y == 0.0 && forward.x == 0.0 )
{
{
yaw = 0.0;
yaw = 0.0
if (forward.z > 0.0)
if (forward.z > 0.0)
pitch = 270.0;
pitch = 270.0
else
else
pitch = 90.0;
pitch = 90.0
}
}
else
else
{
{
yaw = (atan2(forward.y, forward.x) * 180.0 / Constants.Math.Pi);
yaw = (atan2(forward.y, forward.x) * 180.0 / Constants.Math.Pi)
if (yaw < 0.0)
if (yaw < 0.0)
yaw += 360.0;
yaw += 360.0
pitch = (atan2(-forward.z, forward.Length2D()) * 180.0 / Constants.Math.Pi);
pitch = (atan2(-forward.z, forward.Length2D()) * 180.0 / Constants.Math.Pi)
if (pitch < 0.0)
if (pitch < 0.0)
pitch += 360.0;
pitch += 360.0
}
}


return QAngle(pitch, yaw, 0.0);
return QAngle(pitch, yaw, 0.0)
}
}


Line 1,393: Line 1,391:
constructor(_area, _pos, _how)
constructor(_area, _pos, _how)
{
{
area = _area;
area = _area
pos = _pos;
pos = _pos
how = _how;
how = _how
}
}


area = null; // Which area does this point belong to?
area = null // Which area does this point belong to?
pos = null; // Coordinates of the point
pos = null // Coordinates of the point
how = null; // Type of traversal. See Constants.ENavTraverseType
how = null // Type of traversal. See Constants.ENavTraverseType
}
}


Line 1,408: Line 1,406:
function constructor(bot_ent, follow_ent)
function constructor(bot_ent, follow_ent)
{
{
bot = bot_ent;
bot = bot_ent


move_speed = 230.0;
move_speed = 230.0
turn_rate = 5.0;
turn_rate = 5.0
search_dist_z = 128.0;
search_dist_z = 128.0
search_dist_nearest = 128.0;
search_dist_nearest = 128.0


path = [];
path = []
path_index = 0;
path_index = 0
path_reach_dist = 16.0;
path_reach_dist = 16.0
path_follow_ent = follow_ent;
path_follow_ent = follow_ent
path_follow_ent_dist = 50.0;
path_follow_ent_dist = 50.0
path_target_pos = follow_ent.GetOrigin();
path_target_pos = follow_ent.GetOrigin()
path_update_time_next = Time();
path_update_time_next = Time()
path_update_time_delay = 0.2;
path_update_time_delay = 0.2
path_update_force = true;
path_update_force = true
area_list = {};
area_list = {}


seq_idle = bot_ent.LookupSequence("Stand_MELEE");
seq_idle = bot_ent.LookupSequence("Stand_MELEE")
seq_run = bot_ent.LookupSequence("Run_MELEE");
seq_run = bot_ent.LookupSequence("Run_MELEE")
pose_move_x = bot_ent.LookupPoseParameter("move_x");
pose_move_x = bot_ent.LookupPoseParameter("move_x")


debug = true;
debug = true


// Add behavior that will run every tick
// Add behavior that will run every tick
AddThinkToEnt(bot_ent, "BotThink");
AddThinkToEnt(bot_ent, "BotThink")
}
}


Line 1,439: Line 1,437:
{
{
// Clear out the path first
// Clear out the path first
ResetPath();
ResetPath()


// If there is a follow entity specified, then the bot will pathfind to the entity
// If there is a follow entity specified, then the bot will pathfind to the entity
if (path_follow_ent && path_follow_ent.IsValid())
if (path_follow_ent && path_follow_ent.IsValid())
path_target_pos = path_follow_ent.GetOrigin();
path_target_pos = path_follow_ent.GetOrigin()


// Pathfind from the bot's position to the target position
// Pathfind from the bot's position to the target position
local pos_start = bot.GetOrigin();
local pos_start = bot.GetOrigin()
local pos_end = path_target_pos;
local pos_end = path_target_pos


local area_start = NavMesh.GetNavArea(pos_start, search_dist_z);
local area_start = NavMesh.GetNavArea(pos_start, search_dist_z)
local area_end = NavMesh.GetNavArea(pos_end, search_dist_z);
local area_end = NavMesh.GetNavArea(pos_end, search_dist_z)


// If either area was not found, try use the closest one
// If either area was not found, try use the closest one
if (area_start == null)
if (area_start == null)
area_start = NavMesh.GetNearestNavArea(pos_start, search_dist_nearest, false, true);
area_start = NavMesh.GetNearestNavArea(pos_start, search_dist_nearest, false, true)
if (area_end == null)
if (area_end == null)
area_end = NavMesh.GetNearestNavArea(pos_end, search_dist_nearest, false, true);
area_end = NavMesh.GetNearestNavArea(pos_end, search_dist_nearest, false, true)


// If either area is still missing, then bot can't progress
// If either area is still missing, then bot can't progress
if (area_start == null || area_end == null)
if (area_start == null || area_end == null)
return false;
return false


// If the start and end area is the same, one path point is enough and all the expensive path building can be skipped
// If the start and end area is the same, one path point is enough and all the expensive path building can be skipped
if (area_start == area_end)
if (area_start == area_end)
{
{
path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES))
return true;
return true
}
}


// Build list of areas required to get from the start to the end
// Build list of areas required to get from the start to the end
if (!NavMesh.GetNavAreasFromBuildPath(area_start, area_end, pos_end, 0.0, Constants.ETFTeam.TEAM_ANY, false, area_list))
if (!NavMesh.GetNavAreasFromBuildPath(area_start, area_end, pos_end, 0.0, Constants.ETFTeam.TEAM_ANY, false, area_list))
return false;
return false


// No areas found? Uh oh
// No areas found? Uh oh
if (area_list.len() == 0)
if (area_list.len() == 0)
return false;
return false


// Now build points using the list of areas, which the bot will then follow
// Now build points using the list of areas, which the bot will then follow
local area_target = area_list["area0"];
local area_target = area_list["area0"]
local area = area_target;
local area = area_target
local area_count = area_list.len();
local area_count = area_list.len()


// Iterate through the list of areas in order and initialize points
// Iterate through the list of areas in order and initialize points
for (local i = 0; i < area_count && area != null; i++)
for (local i = 0; i < area_count && area != null; i++)
{
{
path.append(PathPoint(area, area.GetCenter(), area.GetParentHow()));
path.append(PathPoint(area, area.GetCenter(), area.GetParentHow()))
area = area.GetParent(); // Advances to the next connected area
area = area.GetParent(); // Advances to the next connected area
}
}


// Reverse the list of path points as the area list is connected backwards
// Reverse the list of path points as the area list is connected backwards
path.reverse();
path.reverse()


// Now compute accurate path points, using adjacent points + direction data from nav
// Now compute accurate path points, using adjacent points + direction data from nav
local path_first = path[0];
local path_first = path[0]
local path_count = path.len();
local path_count = path.len()


// First point is simply our current position
// First point is simply our current position
path_first.pos = bot.GetOrigin();
path_first.pos = bot.GetOrigin()
path_first.how = Constants.ENavTraverseType.NUM_TRAVERSE_TYPES; // No direction specified
path_first.how = Constants.ENavTraverseType.NUM_TRAVERSE_TYPES // No direction specified


for (local i = 1; i < path_count; i++)
for (local i = 1; i < path_count; i++)
{
{
local path_from = path[i - 1];
local path_from = path[i - 1]
local path_to = path[i];
local path_to = path[i]


// Computes closest point within the "portal" between adjacent areas
// Computes closest point within the "portal" between adjacent areas
path_to.pos = path_from.area.ComputeClosestPointInPortal(path_to.area, path_to.how, path_from.pos);
path_to.pos = path_from.area.ComputeClosestPointInPortal(path_to.area, path_to.how, path_from.pos)
}
}


// Add a final point so the bot can precisely move towards the end point when it reaches the final area
// Add a final point so the bot can precisely move towards the end point when it reaches the final area
path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES))
}
}


Line 1,516: Line 1,514:
{
{
// Check for valid path first
// Check for valid path first
local path_len = path.len();
local path_len = path.len()
if (path_len == 0)
if (path_len == 0)
return false;
return false


local path_pos = path[path_index].pos;
local path_pos = path[path_index].pos
local bot_pos = bot.GetOrigin();
local bot_pos = bot.GetOrigin()


// Are we close enough to the path point to consider it as 'reached'?
// Are we close enough to the path point to consider it as 'reached'?
Line 1,527: Line 1,525:
{
{
// Start moving to the next point
// Start moving to the next point
path_index++;
path_index++
if (path_index >= path_len)
if (path_index >= path_len)
{
{
// End of the line!
// End of the line!
ResetPath();
ResetPath()
return false;
return false
}
}
}
}


return true;
return true
}
}


function ResetPath()
function ResetPath()
{
{
area_list.clear();
area_list.clear()
path.clear();
path.clear()
path_index = 0;
path_index = 0
}
}


Line 1,551: Line 1,549:
if (path_update_force)
if (path_update_force)
{
{
UpdatePath();
UpdatePath()
path_update_force = false;
path_update_force = false
}
}
// Recompute path to our target if present
// Recompute path to our target if present
Line 1,558: Line 1,556:
{
{
// Is it time to re-compute the path?
// Is it time to re-compute the path?
local time = Time();
local time = Time()
if (path_update_time_next < time)
if (path_update_time_next < time)
{
{
Line 1,564: Line 1,562:
if ((path_target_pos - path_follow_ent.GetOrigin()).Length() > path_follow_ent_dist)
if ((path_target_pos - path_follow_ent.GetOrigin()).Length() > path_follow_ent_dist)
{
{
UpdatePath();
UpdatePath()
// Don't recompute again for a moment
// Don't recompute again for a moment
path_update_time_next = time + path_update_time_delay;
path_update_time_next = time + path_update_time_delay
}
}
}
}
Line 1,574: Line 1,572:
if (AdvancePath())
if (AdvancePath())
{
{
local path_pos = path[path_index].pos;
local path_pos = path[path_index].pos
local bot_pos = bot.GetOrigin();
local bot_pos = bot.GetOrigin()


// Direction towards path point
// Direction towards path point
local move_dir = (path_pos - bot_pos);
local move_dir = (path_pos - bot_pos)
move_dir.Norm();
move_dir.Norm()


// Convert direction into angle form
// Convert direction into angle form
local move_ang = VectorAngles(move_dir);
local move_ang = VectorAngles(move_dir)


// Approach new desired angle but only on the Y axis
// Approach new desired angle but only on the Y axis
local bot_ang = bot.GetAbsAngles()
local bot_ang = bot.GetAbsAngles()
move_ang.x = bot_ang.x;
move_ang.x = bot_ang.x
move_ang.y = ApproachAngle(move_ang.y, bot_ang.y, turn_rate);
move_ang.y = ApproachAngle(move_ang.y, bot_ang.y, turn_rate)
move_ang.z = bot_ang.z;
move_ang.z = bot_ang.z


// Set our new position and angles
// Set our new position and angles
// Velocity is calculated from direction times speed, and converted from per-second to per-tick time
// Velocity is calculated from direction times speed, and converted from per-second to per-tick time
bot.SetAbsOrigin(bot_pos + (move_dir * move_speed * FrameTime()));
bot.SetAbsOrigin(bot_pos + (move_dir * move_speed * FrameTime()))
bot.SetAbsAngles(move_ang);
bot.SetAbsAngles(move_ang)


return true;
return true
}
}


return false;
return false
}
}


Line 1,609: Line 1,607:
if (bot.GetSequence() != seq_run)
if (bot.GetSequence() != seq_run)
{
{
bot.SetSequence(seq_run);
bot.SetSequence(seq_run)
bot.SetPoseParameter(pose_move_x, 1.0); // Set the move_x pose to max weight
bot.SetPoseParameter(pose_move_x, 1.0) // Set the move_x pose to max weight
}
}
}
}
Line 1,618: Line 1,616:
if (bot.GetSequence() != seq_idle)
if (bot.GetSequence() != seq_idle)
{
{
bot.SetSequence(seq_idle);
bot.SetSequence(seq_idle)
bot.SetPoseParameter(pose_move_x, 0.0); // Clear the move_x pose
bot.SetPoseParameter(pose_move_x, 0.0) // Clear the move_x pose
}
}
}
}
Line 1,625: Line 1,623:
// Replay animation if it has finished
// Replay animation if it has finished
if (bot.GetCycle() > 0.99)
if (bot.GetCycle() > 0.99)
bot.SetCycle(0.0);
bot.SetCycle(0.0)


// Run animations
// Run animations
bot.StudioFrameAdvance();
bot.StudioFrameAdvance()
bot.DispatchAnimEvents(bot);
bot.DispatchAnimEvents(bot)


// Visualize current path in debug mode
// Visualize current path in debug mode
Line 1,636: Line 1,634:
// Stay around for 1 tick
// Stay around for 1 tick
// Debugoverlays are created on 1st tick but start rendering on 2nd tick, hence this must be doubled
// Debugoverlays are created on 1st tick but start rendering on 2nd tick, hence this must be doubled
local frame_time = FrameTime() * 2.0;
local frame_time = FrameTime() * 2.0


// Draw connected path points
// Draw connected path points
local path_len = path.len();
local path_len = path.len()
if (path_len > 0)
if (path_len > 0)
{
{
local path_start_index = path_index;
local path_start_index = path_index
if (path_start_index == 0)
if (path_start_index == 0)
path_start_index++;
path_start_index++


for (local i = path_start_index; i < path_len; i++)
for (local i = path_start_index; i < path_len; i++)
{
{
DebugDrawLine(path[i - 1].pos, path[i].pos, 0, 255, 0, true, frame_time);
DebugDrawLine(path[i - 1].pos, path[i].pos, 0, 255, 0, true, frame_time)
}
}
}
}
Line 1,655: Line 1,653:
foreach (name, area in area_list)
foreach (name, area in area_list)
{
{
area.DebugDrawFilled(255, 0, 0, 30, frame_time, true, 0.0);
area.DebugDrawFilled(255, 0, 0, 30, frame_time, true, 0.0)
DebugDrawText(area.GetCenter(), name, false, frame_time);
DebugDrawText(area.GetCenter(), name, false, frame_time)
}
}
}
}


return 0.0; // Think again next frame
return 0.0 // Think again next frame
}
}


Line 1,667: Line 1,665:
// Change life state to "dying"
// Change life state to "dying"
// The bot won't take any more damage, and sentries will stop targeting it
// The bot won't take any more damage, and sentries will stop targeting it
NetProps.SetPropInt(bot, "m_lifeState", 1);
NetProps.SetPropInt(bot, "m_lifeState", 1)
// Reset health, preventing the default base_boss death behavior
// Reset health, preventing the default base_boss death behavior
bot.SetHealth(bot.GetMaxHealth() * 20);
bot.SetHealth(bot.GetMaxHealth() * 20)
// Custom death behavior can be added here
// Custom death behavior can be added here
// For this example, turn into a ragdoll with the saved damage force
// For this example, turn into a ragdoll with the saved damage force
bot.BecomeRagdollOnClient(damage_force);
bot.BecomeRagdollOnClient(damage_force)
}
}


bot = null; // The bot entity we belong to
bot = null // The bot entity we belong to


move_speed = null; // How fast to move
move_speed = null // How fast to move
turn_rate = null; // How fast to turn
turn_rate = null // How fast to turn
search_dist_z = null; // Maximum distance to look for a nav area downwards
search_dist_z = null // Maximum distance to look for a nav area downwards
search_dist_nearest = null; // Maximum distance to look for any nearby nav area
search_dist_nearest = null // Maximum distance to look for any nearby nav area


path = null; // List of BotPathPoints
path = null // List of BotPathPoints
path_index = null; // Current path point bot is at, -1 if none
path_index = null // Current path point bot is at, -1 if none
path_reach_dist = null; // Distance to a path point to be considered as 'reached'
path_reach_dist = null // Distance to a path point to be considered as 'reached'
path_follow_ent = null; // What entity to move towards
path_follow_ent = null // What entity to move towards
path_follow_ent_dist = null; // Maximum distance after which the path is recomputed
path_follow_ent_dist = null // Maximum distance after which the path is recomputed
// if follow entity's current position is too far from our target position
// if follow entity's current position is too far from our target position
path_target_pos = null; // Position where bot wants to navigate to
path_target_pos = null // Position where bot wants to navigate to
path_update_time_next = null; // Timer for when to update path again
path_update_time_next = null // Timer for when to update path again
path_update_time_delay = null// Seconds to wait before trying to attempt to update path again
path_update_time_delay = null   // Seconds to wait before trying to attempt to update path again
path_update_force = null; // Force path recomputation on the next tick
path_update_force = null // Force path recomputation on the next tick
area_list = null; // List of areas built in path
area_list = null // List of areas built in path


seq_idle = null; // Animation to use when idle
seq_idle = null // Animation to use when idle
seq_run = null; // Animation to use when running
seq_run = null // Animation to use when running
pose_move_x = null; // Pose parameter to set for running animation
pose_move_x = null // Pose parameter to set for running animation


damage_force = null; // Damage force from the bot's last OnTakeDamage event
damage_force = null // Damage force from the bot's last OnTakeDamage event


debug = null; // When true, debug visualization is enabled
debug = null // When true, debug visualization is enabled


}
}
Line 1,707: Line 1,705:
{
{
// Let the bot class handle all the work
// Let the bot class handle all the work
return self.GetScriptScope().my_bot.Update();
return self.GetScriptScope().my_bot.Update()
}
}


Line 1,713: Line 1,711:
{
{
// Find point where player is looking
// Find point where player is looking
local player = GetListenServerHost();
local player = GetListenServerHost()
local trace =
local trace =
{
{
Line 1,719: Line 1,717:
end = player.EyePosition() + (player.EyeAngles().Forward() * 32768.0),
end = player.EyePosition() + (player.EyeAngles().Forward() * 32768.0),
ignore = player
ignore = player
};
}


if (!TraceLineEx(trace))
if (!TraceLineEx(trace))
{
{
printl("Invalid bot spawn location");
printl("Invalid bot spawn location")
return null;
return null
}
}


Line 1,735: Line 1,733:
playbackrate = 1.0, // Required for animations to be simulated
playbackrate = 1.0, // Required for animations to be simulated
health = 300
health = 300
});
})


// Add scope to the entity
// Add scope to the entity
bot.ValidateScriptScope();
bot.ValidateScriptScope()
// Append custom bot class and initialize its behavior
// Append custom bot class and initialize its behavior
bot.GetScriptScope().my_bot <- Bot(bot, player);
bot.GetScriptScope().my_bot <- Bot(bot, player)


         // Fix the default step height which is too high
         // Fix the default step height which is too high
         EntFireByHandle(bot, "SetStepHeight", "18", 0, null, null);
         EntFireByHandle(bot, "SetStepHeight", "18", 0, null, null)


return bot;
return bot
}
}


function OnScriptHook_OnTakeDamage(params)
function OnScriptHook_OnTakeDamage(params)
{
{
local ent = params.const_entity;
local ent = params.const_entity
local inf = params.inflictor;
local inf = params.inflictor
     if (ent.IsPlayer() && HasBotScript(inf) && params.damage_type == 1)
     if (ent.IsPlayer() && HasBotScript(inf) && params.damage_type == 1)
     {
     {
// Don't crush the player if a bot pushes them into a wall
// Don't crush the player if a bot pushes them into a wall
         params.damage = 0;
         params.damage = 0
     }
     }
if (ent.GetClassname() == "base_boss" && HasBotScript(ent))
if (ent.GetClassname() == "base_boss" && HasBotScript(ent))
{
{
// Save the damage force into the bot's data
// Save the damage force into the bot's data
ent.GetScriptScope().my_bot.damage_force = params.damage_force;
ent.GetScriptScope().my_bot.damage_force = params.damage_force
}
}
}
}
Line 1,766: Line 1,764:
function OnGameEvent_npc_hurt(params)
function OnGameEvent_npc_hurt(params)
{
{
local ent = EntIndexToHScript(params.entindex);
local ent = EntIndexToHScript(params.entindex)
if (HasBotScript(ent))
if (HasBotScript(ent))
{
{
Line 1,773: Line 1,771:
{
{
// Run the bot's OnKilled function
// Run the bot's OnKilled function
ent.GetScriptScope().my_bot.OnKilled();
ent.GetScriptScope().my_bot.OnKilled()
}
}
}
}
Line 1,781: Line 1,779:
{
{
// Return true if this entity has the my_bot script scope
// Return true if this entity has the my_bot script scope
return (ent.GetScriptScope() != null && ent.GetScriptScope().my_bot != null);
return (ent.GetScriptScope() != null && ent.GetScriptScope().my_bot != null)
}
}


__CollectGameEventCallbacks(this)
__CollectGameEventCallbacks(this)
BotCreate();
BotCreate()
</source>
</source>



Revision as of 16:27, 12 July 2024

English (en)Русский (ru)Translate (Translate)

Team Fortress 2 This page contains examples of vscripts for Team Fortress 2.

Iterating Through Entities

With awhileloop and a Entities.FindByClassname() function, you can iterate through all entities of a matching classname, based on your arguments.

The first parameter of Entities.FindByClassname() is named 'previous' which accepts a script handle (if an entity inherits 'CBaseEntity' specifically), which verifies if the matching entity it finds has an entity index that's higher than the current one in the 'previous' argument. If it turns out to be not, then its ignored.

local entity = null
while (entity = Entities.FindByClassname(entity, "prop_physics"))
{
   printl(entity)
}

Alternatively, entity iteration loops can also be written in an identical but more compact manner as shown here:

for (local entity; entity = Entities.FindByClassname(entity, "prop_physics");)
{
   printl(entity)
}

Iterating Through Players

It can be useful to iterate through players only. However, doing this with Entities.FindByClassname() is inefficient as it needs to search every entity. A quirk can be utilised to efficiently iterate players. Each networked entity has an associated 'entity index', which ranges from 0 to MAX_EDICTS. Usually these are unpredictable, however there is 2 groups of entities that have reserved entity indexes: worldspawn and players. Worldspawn is always reserved at entity index 0, and players are reserved from entity index 1 to maxplayers + 1. Using this fact, players can be simply iterated like shown below:

Note.pngNote:The old way of iterating players was using Constants.FServers.MAX_PLAYERS. This is no longer recommended. Instead use MaxClients().tointeger() as shown below (only needs to be defined once in any file). MaxClients() only iterates the player limit set in the server rather than the maximum possible amount, which is more efficient, especially now that Team Fortress 2 supports 100 players, but most servers will only be 24/32!
::MaxPlayers <- MaxClients().tointeger()

for (local i = 1; i <= MaxPlayers ; i++)
{
    local player = PlayerInstanceFromIndex(i)
    if (player == null) continue
    printl(player)
}

Iterating Through Player's Weapons

The maximum amount of weapons a player can hold under normal gameplay scenarios is 8. (As Engineer: primary, secondary, melee, construction PDA, destruction PDA, the toolbox, grappling hook and the 'passtime gun' when holding passtime_ball)

::MAX_WEAPONS <- 8

for (local i = 0; i < MAX_WEAPONS; i++)
{
    local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
    if (weapon == null)
        continue
    printl(weapon)
}

Iterating Through Player's Cosmetics

Cosmetics are called "wearables" internally.

for (local wearable = player.FirstMoveChild(); wearable != null; wearable = wearable.NextMovePeer())
{
    if (wearable.GetClassname() != "tf_wearable")
        continue
    printl(wearable)
}

Spawning an Entity

The following code shows to spawn an entity, specifically a rocket. The rocket will be spawned in front of the first available player.

// By ZooL_Smith

local ply = Entities.FindByClassname(null, "player")

local rocket = SpawnEntityFromTable("tf_projectile_rocket", 
{
    // This is a table of keyvalues, which is the same way as keyvalues that are defined in Hammer
    // Key         Value
    basevelocity = ply.EyeAngles().Forward()*250,
    teamnum      = ply.GetTeam(),
    origin       = ply.EyePosition()+ply.EyeAngles().Forward()*32,
    angles       = ply.EyeAngles()
})

rocket.SetOwner(ply) // make it not collide with owner and give proper kill credits

Swapping the !activator's Team

Whenever a script's code gets executed by inputs (e.g.,RunScriptCode/CallScriptFunctionon an entity with an attached entity script), the activator and caller variables will be set to the handles of the input's !activator and !caller respectively, allowing you to access them in code.

We can use that to easily do something to players that walk in a specific trigger, for example.

As for swapping the !activator's team, we need to do both change their team based on which team they are, and then also change the their cosmetic items' team to the their's new team.

First we need to figure on which team the !activator is. For that, we can use the GetTeam() method on the !activator (activator.GetTeam()) to get the number index of their team. You can either use an if function to compare the returned value with the desired team index, or store it in a variable to use it later. The latter in this case may be better to reduce the length of the script. For reference: 0 is Unassigned (no team yet), 1 is Spectator, 2 is Red, 3 is Blu.

To change the !activator's team, we should use the ForceChangeTeam(Int : Team index, Bool : Kill player if game is in Highlander mode + remove their dominations) method on them via activator.ForceChangeTeam(...).

To change their cosmetic items' color, we need to iterate over every tf_wearable entity, and then change its team via SetTeam(Int : Team index) if they are worn by the !activator.

  • To iterate, we need to specify a null variable (e.g. local cosmetic = null), and then pass it into the following while loop:
local cosmetic = null // Assign the "cosmetic" variable to null, will be assigned new values when going over cosmetic items
while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable"))
{
    // Do things to individual tf_wearables entities here, the "cosmetic" variable is set to the current tf_wearable entity we are iterating through
}
  • To check if the cosmetic item belongs to the !activator, we can simply compare the value returned by the GetOwner() method when we use it on the tf_wearable (e.g. cosmetic.GetOwner()) against the !activator. Example code:
if (cosmetic.GetOwner() == activator)
{
    // Do things if the cosmetic's wearer is the !activator
}
  • To change the cosmetic's team, we should use the SetTeam(Int : Team index) method on the cosmetic item entity.

Full code with comments:

function ActivatorSwapTeam() // Call this in map via RunScriptCode > ActivatorSwapTeam() or CallScriptFunction > ActivatorSwapTeam on a logic_script that has an Entity Script with this function
{
    // The following snippet checks if an !activator has been specified and if they are a player
    // If either question's answer is no, then don't execute the rest of the function
    if (activator == null || activator.IsPlayer() == false)
    {
        return
    }
    
    // The following snippet compares the !activator's team number (Ranging from 0 to 3) to the ones used by Unassigned (0) and Spectator (1)
    // If they match with either Unassigned or Spectator, we don't execute the rest of the function
    // Used to ignore any potentional spectator !activators
    if (activator.GetTeam() == 0 || activator.GetTeam() == 1)
    {
        return
    }
    
    // The following snippet specifies a local newTeam variable, and then we set it to a team number based off the !activator's current team number
    local newTeam = 0
    if (activator.GetTeam() == 2) // Checks if the !activator's team number is 2 (Red), and sets the newTeam variable to 3 (Blu)
    {   
        newTeam = 3
    } else { // If the !activator's team number is not 2 (Red), sets the newTeam variable to 2 (Blu) instead
        newTeam = 2
    }

    // The following snippet calls the ForceChangeTeam method on the !activator
    // First parameter: Team number to switch to
    // Second parameter: If false, the game will reset the player's dominations and nemesises, and kill them if mp_highlander is on
    activator.ForceChangeTeam(newTeam, true)
    
    local cosmetic = null // Assign the "cosmetic" variable to null, will be assigned new values when going over cosmetic items
    // The following snippet will go over every cosmetic item currently present, and will change its colours to the appropriate team if they are the !activator's
    while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable")) // Goes over every cosmetic item, executing code below
    {
        if (cosmetic.GetOwner() == activator) // Checks if the currently iterated cosmetic item's wearer is the !activator
        {
            cosmetic.SetTeam(newTeam) // Sets the team of the cosmetic item to the new team number that we stored in newTeam
        }
    }
}

Listening for Events

Many actions in the game fire events to notify other parts of code that something has happened. For example, when a player dies or when an Engineer's building is sapped. Each event can also hold certain type of data, such as the index of an involved entity.

VScript can catch these events, parse the data and run some code when they happen (known as a 'callback'). The list of available events can be found here:

Warning.pngWarning:Event callbacks are global and do not get cleared on round restart. Event callbacks should be cleared via ClearGameEventCallbacks() to prevent them from stacking up between rounds.

The following code shows how to listen for the post_inventory_application event, then add uber protection for 2 seconds.

// The event "post_inventory_application" is sent when a player gets a whole new set of items, aka touches a resupply locker / respawn cabinet or spawns in.
function OnGameEvent_post_inventory_application(params)
{
	local player = GetPlayerFromUserID(params.userid)

	// add uber protection to the player for 2 seconds.
	player.AddCondEx(Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF, 2.0, null)
}

__CollectGameEventCallbacks(this)

Adding attributes to player on spawn

Intuitively, applying attributes to the player on the player_spawn event should work. Unfortunately this doesn't work in practice, as the game clears all custom attributes immediately after this event is fired. As a workaround, the attributes can be postponed to be added at the end of the frame, as shown below:

Note.pngNote:This assumes event callbacks are setup already, like in the example above.
::PostPlayerSpawn <- function()
{
    // "self" is the player entity here
    self.AddCustomAttribute("no jump", 1, -1)
}

function OnGameEvent_player_spawn(params)
{
    local player = GetPlayerFromUserID(params.userid)
    if (player != null)
    {
        EntFireByHandle(player, "CallScriptFunction", "PostPlayerSpawn", 0, null, null)
    }
}

Think functions / reading player input

A think function is a function on an entity that runs repeatedly on a timer. How it works:

  • Define a function with no parameters
  • Specify the think function name using the Script Think Function or add it to the entity using AddThinkToEnt
  • The return value of the function specifies the delay before running it again. By default, this is 0.1 seconds. The fastest possible delay is per-tick by returning -1. Team Fortress 2 runs at 66 ticks per second, so this is effectively a 0.015 interval.

The example below when executed will show a message when the player presses reload.

Note.pngNote:Always use a per-tick think function for reading input.
function PlayerThink()
{
	local buttons = NetProps.GetPropInt(self, "m_nButtons")
	if (buttons & Constants.FButtons.IN_RELOAD)
	{
		printl("Player is reloading")
	}
	
	return -1
}

local player = GetListenServerHost()
AddThinkToEnt(player, "PlayerThink")
Note.pngNote:Think functions must be in global scope or in the entity's scope for it to work. The example above would not work in a logic_script because the PlayerThink function would not exist outside of the assigned entity. To fix this, either
  • Make PlayerThink global, e.g. rewrite as ::PlayerThink <- function()

or

  • Add PlayerThink to the entity's scope, e.g. player.GetScriptScope().PlayerThink <- PlayerThink

It may be useful to only detect when a key is pressed (or released) rather than when it's being helded. This can be achieved by storing the last button state and comparing it against the new one. The example below prints a message when the player left-clicks.

function PlayerThink()
{
	local buttons = NetProps.GetPropInt(self, "m_nButtons")
	local buttons_changed = buttons_last ^ buttons
	local buttons_pressed = buttons_changed & buttons
	local buttons_released = buttons_changed & (~buttons)
	
	if (buttons_pressed & Constants.FButtons.IN_ATTACK)
	{
		printl("Player pressed left-click")
	}
	
	buttons_last = buttons
	return -1
}

local player = GetListenServerHost()
player.ValidateScriptScope()
player.GetScriptScope().buttons_last <- 0
AddThinkToEnt(player, "PlayerThink")

Setting up a Boss Health Bar

The boss bar that appears while any bosses are active (such as MONOCULUS!) is handled by the monster_resource entity, which conveniently exists in the map normally. You can do Entities.FindByClassname(null, "monster_resource") to get the monster_resource entity most of the time, but it'd be convenient to store it into a variable instead.

In order to modify the health bar's percentage, it'd be useful to first know about NetProps:

  • NetProps are network properties of an entity, which are server-side only.
  • They can be accessed and changed by the NetProps class methods.

The monster_resource entity has a m_iBossHealthPercentageByte NetProp, which determines the percentage state of the health bar based on a byte value - its value must be between 0 and 255, where 0 is 0%, and 255 is 100%

The following code will add a health bar with 25% health after executed.

local healthBar = Entities.FindByClassname(null, "monster_resource") // Get the health bar entity.
if (healthBar) { // Check if the health bar entity exists, just in case to prevent errors 
    // The following line will update the health bar's percentage to 25% by changing its NetProp.
    // Do note that because it is a byte (0 - 255), we need to multiply our percentage by 255.
    NetProps.SetPropInt(healthBar, "m_iBossHealthPercentageByte", 0.25 * 255)
}

Toggling HUD Elements

Some HUD elements can be hidden away for players by using the appropriate HudHideFlags functions. The available flags can be found on the Constants page. Since these are bit flags, elements to hide or show can be combined using the OR | operator.

Example usage:

player.AddHudHideFlags(
	Constants.FHideHUD.HIDEHUD_CROSSHAIR |
	Constants.FHideHUD.HIDEHUD_HEALTH |
	Constants.FHideHUD.HIDEHUD_WEAPONSELECTION
) // hides hud elements

player.RemoveHudHideFlags(player,
	Constants.FHideHUD.HIDEHUD_CROSSHAIR |
	Constants.FHideHUD.HIDEHUD_HEALTH |
	Constants.FHideHUD.HIDEHUD_WEAPONSELECTION
) // makes hud elements visible again

Line 2 would add bits to hide the crosshair, health, and disabling weapon switching from the player. Line 3 would remove bits to show the crosshair, health, and re-enable weapon switching for the player.

Checking if entity or player is alive

::IsEntityAlive <- function(entity)
{
    // lifeState corresponds to the following values:
    // 0 - alive
    // 1 - dying (probably unused)
    // 2 - dead
    // 3 - respawnable (spectating)
    return NetProps.GetPropInt(entity, "m_lifeState") == 0
}

Fetching player name or Steam ID

Steam IDs are stored in SteamID3 format, e.g. [U:1:53275741]

::GetPlayerName <- function(player)
{
    return NetProps.GetPropString(player, "m_szNetname")
}

::GetPlayerSteamID <- function(player)
{
    return NetProps.GetPropString(player, "m_szNetworkIDString")
}

Fetching and setting entity color

::SetEntityColor <- function(entity, r, g, b, a)
{
    local color = (r) | (g << 8) | (b << 16) | (a << 24)
    NetProps.SetPropInt(entity, "m_clrRender", color)
}

::GetEntityColor <- function(entity)
{
    local color = NetProps.GetPropInt(entity, "m_clrRender")
    return
    {
        r = color & 0xFF,
        g = (color >> 8) & 0xFF,
        b = (color >> 16) & 0xFF,
        a = (color >> 24) & 0xFF,
    }
}

Giving a weapon

See this page to find corresponding classname and itemIDs for each weapon.

Warning.pngWarning:This will cause a client crash when giving an Engineer toolbox (tf_weapon_builder with itemID 28). To fix this, you must set the m_iObjectType and m_iObjectMode netprops to 0 before equipping. You also need to set the m_aBuildableObjectTypes netprop array to allow objects to be built.
::MAX_WEAPONS <- 8

::GivePlayerWeapon <- function(player, className, itemID)
{
    local weapon = Entities.CreateByClassname(className)
    NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", itemID)
    NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
    NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true)
    weapon.SetTeam(player.GetTeam())
    weapon.DispatchSpawn()

    // remove existing weapon in same slot
    for (local i = 0; i < MAX_WEAPONS; i++)
    {
        local heldWeapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
        if (heldWeapon == null)
            continue
        if (heldWeapon.GetSlot() != weapon.GetSlot())
            continue
        heldWeapon.Destroy()
        NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i)
        break
    }
    
    player.Weapon_Equip(weapon)
    player.Weapon_Switch(weapon)

    return weapon
}

GivePlayerWeapon(GetListenServerHost(), "tf_weapon_smg", 16)

Creating Mannpower powerups

Creating any type of Mannpower powerup other than Strength is usually not possible manually. However, players are able to drop powerups, which can therefore be manipulated to drop any kind of powerup. The following example abstracts this to allow creating a powerup with a given position, angles, velocity, team and type. The powerup can also be set not to "re-position" (blink and disappear) after time has passed, unlike usual dropped powerups.

Example usage:

CreateRune(Vector(0, 0, -100), QAngle(0, 0, 0), Vector(-500, 0, 0), Constants.ETFTeam.TEAM_ANY, ERuneTypes.RUNE_VAMPIRE, false)


Note.pngNote:This code converts existing powerup classnames from item_powerup_rune to zitem_powerup_rune for speed.
enum ERuneTypes
{
    RUNE_STRENGTH,
    RUNE_HASTE,
    RUNE_REGEN,
    RUNE_RESIST,
    RUNE_VAMPIRE,
    RUNE_REFLECT,
    RUNE_PRECISION,
    RUNE_AGILITY,
    RUNE_KNOCKOUT,
    RUNE_KING,
    RUNE_PLAGUE,
    RUNE_SUPERNOVA,
}

::RuneTypeToCond <-
[
	Constants.ETFCond.TF_COND_RUNE_STRENGTH,
	Constants.ETFCond.TF_COND_RUNE_HASTE,
	Constants.ETFCond.TF_COND_RUNE_REGEN,
	Constants.ETFCond.TF_COND_RUNE_RESIST,
	Constants.ETFCond.TF_COND_RUNE_VAMPIRE,
	Constants.ETFCond.TF_COND_RUNE_REFLECT,
	Constants.ETFCond.TF_COND_RUNE_PRECISION,
	Constants.ETFCond.TF_COND_RUNE_AGILITY,
	Constants.ETFCond.TF_COND_RUNE_KNOCKOUT,
	Constants.ETFCond.TF_COND_RUNE_KING,
	Constants.ETFCond.TF_COND_RUNE_PLAGUE,
	Constants.ETFCond.TF_COND_RUNE_SUPERNOVA,
]

::MaxPlayers <- MaxClients().tointeger()

::CreateRune <- function(origin, angles, velocity, team, type, reposition)
{
	// select random player to create a rune from
	// prioritize players with no rune, as stripping the rune temporarily can have side effects
	local player, fallback
	for (local i = 1; i <= MaxPlayers; i++)
	{
		player = PlayerInstanceFromIndex(i)
		if (player)
		{
			if (player.IsCarryingRune())
			{
				fallback = player
				player = null
				continue
			}
			
			break
		}
	}
	
	if (!player)
	{
		if (!fallback)
			return null
		player = fallback
	}
		
	// to detect the rune that was spawned, every existing rune must be hidden
	for (local rune; rune = Entities.FindByClassname(rune, "item_powerup_rune");)
		rune.KeyValueFromString("classname", "zitem_powerup_rune")
	
	local cond = RuneTypeToCond[type]
	
	// if player already has a rune, temporarily strip it
	local player_cond, player_cond_duration
	if (player.IsCarryingRune())
	{
		foreach (cond in RuneTypeToCond)
		{
			player_cond_duration = player.GetCondDuration(cond)
			if (player_cond_duration != 0.0)
			{
				player.RemoveCond(cond)
				player_cond = cond
				break
			}
		}	
	}

	local cond_prop = "m_Shared." + (cond >= 96 ? "m_nPlayerCondEx3" : "m_nPlayerCondEx2")
	local cond_bits = NetProps.GetPropInt(player, cond_prop)
	NetProps.SetPropInt(player, cond_prop, cond_bits | (1 << (cond % 32)))
	player.DropRune(false, team)
	NetProps.SetPropInt(player, cond_prop, cond_bits)
	
	// give original rune back
	if (player_cond)
		player.AddCondEx(player_cond, player_cond_duration, null)
	
	local rune = Entities.FindByClassname(null, "item_powerup_rune")
	if (!rune)
		return null
	
	rune.Teleport(true, origin, true, angles, true, velocity)
	
	if (!reposition)
	{
		// prevents rune from blinking after 30 or 60 seconds
		// and teleporting to a spawnpoint if one exists
		rune.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION)
		rune.AddSolidFlags(Constants.FSolid.FSOLID_TRIGGER)
	}
	
	return rune
}

Detecting melee smacks / creating projectiles with damage

This example demonstrates how to reliably detect when a melee has fired, by abusing a quirk in one of the melee's internal variables. When a Heavy attacks with their fists, they will emit a fireball like from the Dragon's Fury. Therefore this example also demonstrates how to create projectiles that deal damage which can be problematic by usual means.

if (!("FireballMaker" in getroottable()) || !FireballMaker.IsValid())
{
	// spawn a fake dragon's fury to emit the fireballs
	FireballMaker <- Entities.CreateByClassname("tf_weapon_rocketlauncher_fireball")
	NetProps.SetPropInt(FireballMaker, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 1178)
	NetProps.SetPropBool(FireballMaker, "m_AttributeManager.m_Item.m_bInitialized", true)
	FireballMaker.DispatchSpawn()
	FireballMaker.SetClip1(-1)
}

function CheckMeleeSmack()
{
	local owner = self.GetOwner()
	// when melee smacks, m_iNextMeleeCrit is 0
	if (NetProps.GetPropInt(owner, "m_Shared.m_iNextMeleeCrit") == 0)
	{
		// when switching away from melee, m_iNextMeleeCrit will also be 0 so check for that case
		if (owner.GetActiveWeapon() == self)
		{
			// preserve old charge meter and ammo count
			local charge = NetProps.GetPropFloat(owner, "m_Shared.m_flItemChargeMeter")
			local ammo = NetProps.GetPropIntArray(owner, "m_iAmmo", 1)
			
			// set up stuff needed to ensure the weapon always fires
			NetProps.SetPropIntArray(owner, "m_iAmmo", 99, 1)
			NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", 100.0)
			NetProps.SetPropBool(owner, "m_bLagCompensation", false)
			NetProps.SetPropFloat(FireballMaker, "m_flNextPrimaryAttack", 0)
			NetProps.SetPropEntity(FireballMaker, "m_hOwner", owner)

			FireballMaker.PrimaryAttack()
			
			// revert changes
			NetProps.SetPropBool(owner, "m_bLagCompensation", true)
			NetProps.SetPropIntArray(owner, "m_iAmmo", ammo, 1)
			NetProps.SetPropFloat(owner, "m_Shared.m_flItemChargeMeter", charge)
		}
		
		// continue smack detection
		NetProps.SetPropInt(owner, "m_Shared.m_iNextMeleeCrit", -2)
	}
	
	return -1
}

ClearGameEventCallbacks()

function OnGameEvent_player_spawn(params)
{
	local player = GetPlayerFromUserID(params.userid)
	if (!player)
		return
	
	local player_class = params["class"]
	if (player_class != Constants.ETFClass.TF_CLASS_HEAVYWEAPONS)
		return
	
	for (local i = 0; i < 8; i++)
	{
		local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
		if (weapon == null || !weapon.IsMeleeWeapon())
			continue
			
		// this kicks off melee smack detection logic
		NetProps.SetPropInt(player, "m_Shared.m_iNextMeleeCrit", -2)
		AddThinkToEnt(weapon, "CheckMeleeSmack")
		break
	}
}

__CollectGameEventCallbacks(this)

Detecting weapon firing

Similar to above example, on all weapons except melee there is another variable that can be tracked. This example pushes the player back when they fire their weapon.

function CheckWeaponFire()
{
	local fire_time = NetProps.GetPropFloat(self, "m_flLastFireTime")
	if (fire_time > last_fire_time)
	{
		printf("%f %s : Fired\n", Time(), self.GetClassname())
		
		local owner = self.GetOwner()
		if (owner)
		{
			owner.SetAbsVelocity(owner.GetAbsVelocity() - owner.EyeAngles().Forward() * 800.0)
		}
		
		last_fire_time = fire_time
	}
	return -1
}

ClearGameEventCallbacks()

function OnGameEvent_player_spawn(params)
{
	local player = GetPlayerFromUserID(params.userid)
	if (!player)
		return
	
	for (local i = 0; i < 8; i++)
	{
		local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
		if (weapon == null || weapon.IsMeleeWeapon())
			continue
			
		weapon.ValidateScriptScope()
		weapon.GetScriptScope().last_fire_time <- 0.0
		weapon.GetScriptScope().CheckWeaponFire <- CheckWeaponFire
		AddThinkToEnt(weapon, "CheckWeaponFire")
	}
}

__CollectGameEventCallbacks(this)

Disabling Medic health regen

This effectively cancels out the passive health regeneration from a medic.

function OnGameEvent_player_healed(params)
{
    if (params.patient == params.healer)
    {
        local player = GetPlayerFromUserID(params.patient)
        if (player 
            && player.GetPlayerClass() == Constants.ETFClass.TF_CLASS_MEDIC
            && NetProps.GetPropInt(player, "m_bitsDamageType") == 32)
        {
            player.SetHealth(player.GetHealth() - params.amount)
        }
    }
}

Special death effects on triggers

Weapons that have special death effects (such as disintegration from Phlogistinator, or the ice statue from Spy-cicle) can be applied to triggers via two methods:

Using the TakeDamageCustom function

This method will work for most listed custom damage types, e.g. decapitation or Cow Mangler's dissolve effect. Otherwise, the other method must be used.

Add the following output to a trigger_hurt. Replace 9999 with the desired damage and TF_DMG_CUSTOM part with the desired effect.

OnStartTouch | !activator | RunScriptCode | self.TakeDamageCustom(activator, activator, null, Vector(), Vector(), 9999, 0, Constants.ETFDmgCustom.TF_DMG_CUSTOM_PLASMA)

Using a dummy weapon

Some death effects are not available as a custom damage type, but they can be applied by using a spoofed weapon as the damage owner.

The example below shows how to create a trigger that kills players with a disintegration effect when touched. This script can be assigned to any trigger entity. Note that if the trigger is not a trigger_hurt, Engineer buildings will be possible to place inside. This can be workarounded by placing a func_nobuild in it's place as well.

// Ensure only one of this entity is ever spawned
if (!("disintegrate_proxy_weapon" in getroottable()))
{
    ::disintegrate_proxy_weapon <- null
	
    ::disintegrate_immune_conds <- 
    [
        Constants.ETFCond.TF_COND_INVULNERABLE,
        Constants.ETFCond.TF_COND_PHASE,
        Constants.ETFCond.TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED,
        Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
        Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
    ]
}

// Create the fake weapon if one doesn't already exist
if (!disintegrate_proxy_weapon || !disintegrate_proxy_weapon.IsValid())
{
    disintegrate_proxy_weapon = Entities.CreateByClassname("tf_weapon_bat")
    NetProps.SetPropInt(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 349)
    NetProps.SetPropBool(disintegrate_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
    disintegrate_proxy_weapon.DispatchSpawn()
	disintegrate_proxy_weapon.DisableDraw()
	
	// Add the attribute that creates disintegration
    disintegrate_proxy_weapon.AddAttribute("ragdolls become ash", 1.0, -1.0)
}

function Precache()
{
	// Add an output to deal damage when the trigger is touched
    self.ConnectOutput("OnStartTouch", "Disintegrate")
}

function Disintegrate()
{
	// Remove conditions that give immunity to damage
    foreach (cond in disintegrate_immune_conds)
        activator.RemoveCondEx(cond, true)
        
	// Set any owner on the weapon to prevent a crash
    NetProps.SetPropEntity(disintegrate_proxy_weapon, "m_hOwner", activator)
	
	// Deal the damage with the weapon
    activator.TakeDamageCustom(self, activator, disintegrate_proxy_weapon, 
                                Vector(0,0,0), Vector(0,0,0), 
                                99999.0, 2080, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BURNING)
}

Similar example but creates an ice statue on death instead.

// Ensure only one of this entity is ever spawned
if (!("freeze_proxy_weapon" in getroottable()))
{
    ::freeze_proxy_weapon <- null
	
    ::freeze_immune_conds <- 
    [
        Constants.ETFCond.TF_COND_INVULNERABLE,
        Constants.ETFCond.TF_COND_PHASE,
        Constants.ETFCond.TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED,
        Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF,
        Constants.ETFCond.TF_COND_INVULNERABLE_CARD_EFFECT,
    ]
}

// Create the fake weapon if one doesn't already exist
if (!freeze_proxy_weapon || !freeze_proxy_weapon.IsValid())
{
    freeze_proxy_weapon = Entities.CreateByClassname("tf_weapon_knife")
    NetProps.SetPropInt(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 649)
    NetProps.SetPropBool(freeze_proxy_weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
    freeze_proxy_weapon.DispatchSpawn()
	freeze_proxy_weapon.DisableDraw()
	
	// Add the attribute that creates ice statues
	freeze_proxy_weapon.AddAttribute("freeze backstab victim", 1.0, -1.0)
}

function Precache()
{
	// Add an output to deal damage when the trigger is touched
    self.ConnectOutput("OnStartTouch", "Freeze")
}

function Freeze()
{
	// Remove conditions that give immunity to damage
    foreach (cond in freeze_immune_conds)
        activator.RemoveCondEx(cond, true)
        
	// Set any owner on the weapon to prevent a crash
    NetProps.SetPropEntity(freeze_proxy_weapon, "m_hOwner", activator)
	
	// Deal the damage with the weapon
    activator.TakeDamageCustom(self, activator, freeze_proxy_weapon, 
                                Vector(0,0,0), Vector(0,0,0), 
                                99999.0, Constants.FDmgType.DMG_CLUB, Constants.ETFDmgCustom.TF_DMG_CUSTOM_BACKSTAB)
		
	// I don't remember why this is needed but it's important
	local ragdoll = NetProps.GetPropEntity(activator, "m_hRagdoll")
	if (ragdoll)
		NetProps.SetPropInt(ragdoll, "m_iDamageCustom", 0)
}

Printing colored text from Hammer I/O

When attempting to print colored text in Hammer using entity I/O, the escape characters used will not be saved correctly. While Hammer++ does support escape characters for newlines, the \x07 sequence for colored text will crash the editor when loading the map. To work around this, you can make a utility function to replace a random character with \x07 at run-time.

Note.pngNote:Does not support alpha, you will need to modify this function to replace the character with \x08 instead of \x07.
//print colored text within hammer
::ClientPrintSafe <- function(player, text)
{
    //replace ^ with \x07 at run-time
    local escape = "^"

    //just use the normal print function if there's no escape character
    if (!startswith(text, escape)) 
    {
        ClientPrint(player, 3, text)
        return
    }
    
    //split text at the escape character
    local splittext = split(text, escape)

    //remove 0-length strings
    for (local i = splittext.len() - 1; i >= 0; i--)
        if (splittext[i].len() < 1) 
            splittext.remove(i)
    
    //format into new string
    local formatted = ""
    foreach (i, t in splittext)
        formatted += format("\x07%s", t)
    
    //print formatted string
    ClientPrint(player, 3, formatted)
}

ClientPrintSafe(null, "^FF0000This text is red^")

Getting the userid from a player handle

::PlayerManager <- Entities.FindByClassname(null, "tf_player_manager")
::GetPlayerUserID <- function(player)
{
    return NetProps.GetPropIntArray(PlayerManager, "m_iUserID", player.entindex())
}

Improving player collisions on physics objects or trains

Team Fortress 2 Team Fortress 2 has a subdued physics mode by default, which makes players poorly interact with physics objects unlike other Source games. This can be fixed by setting the sv_turbophysics convar to 0.

Convars.SetValue("sv_turbophysics", 0)

func_tracktrains have similar problems. The game has a special train physics mode for Payload to circumvent this issue, which is normally not enabled in other gamemodes. This can be forcefully enabled (or disabled!) via the following:

ForceEscortPushLogic(2)

Moving a player into spectating state without death

Avoids death effects such as kill feed or screams from appearing if killed by conventional means, and the player still remains on the team.

NetProps.SetPropInt(player, "m_iObserverLastMode", 5)
local team = player.GetTeam()
NetProps.SetPropInt(player, "m_iTeamNum", 1)
player.DispatchSpawn()
NetProps.SetPropInt(player, "m_iTeamNum", team)

Finding changed netprops

It may be useful to see what netprops have changed after an event occurs on an entity. The following example shows the changed sendprops (networked changes) and datamaps (server-specific changes) after the local player is killed.

function DumpChangedProps(ent)
{
	local prev_sendprops = {}
	local prev_datamaps = {}
	local cur_sendprops = {}
	local cur_datamaps = {}
	
	NetProps.GetTable(ent, 0, prev_sendprops)
	NetProps.GetTable(ent, 1, prev_datamaps)
	
	ent.TakeDamage(999.9, 32, null)
	
	NetProps.GetTable(ent, 0, cur_sendprops)
	NetProps.GetTable(ent, 1, cur_datamaps)

	local recursive_compare
	recursive_compare = function(prev_table, cur_table, indent="")
	{
		foreach (k, v in cur_table)
		{
			local prev_v = prev_table[k]
			
			if (typeof(v) == "table")
			{
				printl(indent + "TABLE: " + k)
				recursive_compare(prev_table[k], v, indent + "\t")
			}
			else 
			{
				if (typeof(v) == "Vector" || typeof(v) == "QAngle")
				{
					if (v.x != prev_v.x || v.y != prev_v.y || v.z != prev_v.z)
						printl(indent + k + " changed: " + prev_v + " -> " + v)
				}
				else 
				{
					if (prev_v != v)
						printl(indent + k + " changed: " + prev_v + " -> " + v)
				}
			}
		}
	}
	
	recursive_compare(prev_sendprops, cur_sendprops)
	recursive_compare(prev_datamaps, cur_datamaps)
}

DumpChangedProps(GetListenServerHost())


Force Change Class

When doing server-only scripts via mapspawn.nut, calling SetPlayerClass can either plain not work or work buggily. For example, looking at your loadout shows the class you tried to choose, rather than forced class and each time you spawn your class will revert back to what you tried to select. By also updating m_iDesiredPlayerClass, these aforementioned bugs no longer occur.

::ForceChangeClass <- function(player, classIndex) {
	player.SetPlayerClass(classIndex)
	NetProps.SetPropInt(player, "m_Shared.m_iDesiredPlayerClass", classIndex)
	player.ForceRegenerateAndRespawn()
}
ForceChangeClass(GetListenServerHost(), Constants.ETFClass.TF_CLASS_SCOUT)

Change how long it takes to capture a control point

When doing server-only scripts via mapspawn.nut, you may want to increase the amount of time it takes to capture a control point. The following snippet increases the time it takes to capture a control point by 2x.

function OnGameEvent_teamplay_round_start(params) {
	// Must set this when the map resets only, otherwise the scaling compounds between rounds
	// If this is set elsewhere, it may not work as the m_flCapTime gets reset.
	if (params.full_reset) {
		for (local ent = null; ent = Entities.FindByClassname(ent, "trigger_capture_area");) {
			local capPointName = NetProps.GetPropString(ent, "m_iszCapPointName")
			local capTimeInSeconds = NetProps.GetPropFloat(ent, "m_flCapTime")
			// Double the time it takes to capture a control point
			capTimeInSeconds *= 2
			NetProps.SetPropFloat(ent, "m_flCapTime", capTimeInSeconds)
			// If we do not fire this event, then cap time total does not propagate to the client
			// and the UI will be buggy and show the control point capping quicker then it should
			// and then "popping" back after the server updates the client.
			EntFireByHandle(ent, "SetControlPoint", capPointName, 0.0, null, null)
		}
	}
}

Making a model visible to only to a specific player

In the game, entities are usually networked to every player. It may be useful to show a model to a specific player only, which is what this example allows. Engineer buildings only show to their owner during their "blueprint" phase, and this fact can be abused to make a model only show to a desired player.

local proxy_entity = Entities.CreateByClassname("obj_teleporter") // not using SpawnEntityFromTable as that creates spawning noises
proxy_entity.SetAbsOrigin(Vector(0, -320, -150))
proxy_entity.DispatchSpawn()
proxy_entity.SetModel("models/player/heavy.mdl")
proxy_entity.AddEFlags(Constants.FEntityEFlags.EFL_NO_THINK_FUNCTION) // prevents the entity from disappearing
proxy_entity.SetSolid(Constants.ESolidType.SOLID_NONE)
NetProps.SetPropBool(proxy_entity, "m_bPlacing", true)
NetProps.SetPropInt(proxy_entity, "m_fObjectFlags", 2) // sets "attachment" flag, prevents entity being snapped to player feet

// m_hBuilder is the player who the entity will be networked to only
NetProps.SetPropEntity(proxy_entity, "m_hBuilder", GetListenServerHost())

Checking if point or box is inside a respawn room / tracing triggers

This abuses a quirk where triggers can be set solid temporarily so traces "hit" them, and reverted afterwards. The example below demonstrates this to detect if a point is inside a respawn room.

Example usage: printl(IsPointInRespawnRoom(GetListenServerHost().EyePosition()))

Note.pngNote:The SetCollisionGroup lines are only necessary for func_respawnroom. Other triggers don't need those lines.
function IsPointInRespawnRoom(point)
{
	local triggers = []
	for (local trigger; trigger = Entities.FindByClassname(trigger, "func_respawnroom");)
	{
		trigger.SetCollisionGroup(0)
		trigger.RemoveSolidFlags(4) // FSOLID_NOT_SOLID
		triggers.append(trigger)
	}
	
	local trace =
	{
		start = point,
		end = point,
		mask = 0
	}
	TraceLineEx(trace)
	
	foreach (trigger in triggers)
	{
		trigger.SetCollisionGroup(25) // special collision group used by respawnrooms only
		trigger.AddSolidFlags(4) // FSOLID_NOT_SOLID
	}

	return trace.hit && trace.enthit.GetClassname() == "func_respawnroom"
}

Enabling the Spell HUD and Effects

By setting the m_bIsUsingSpells netprop to true, you can Enable the spell HUD, the sound effects for rolling spells, and set the spellbook magazine to the default action slot item. Note that spells do work when this netprop is set to false, however the HUD elements/sound effects will not appear, and players will need to manually equip the spellbook if they have not already.

::SetSpellHUD <- function(value)
{
    local gamerules = Entities.FindByClassname(null, "tf_gamerules")
    NetProps.SetPropBool(gamerules, "m_bIsUsingSpells", value)
}
SetSpellHUD(true)

Changing Player Teams in MvM

When attempting to change to change the player to the blue team in MvM using ForceChangeTeam or SetTeam, they will be immediately switched back to RED. To circumvent this, you can temporarily change the tf_gamerules NetProp m_bPlayingMannVsMachine to false, switch the player team, then set it back to true immediately after. Note that you will need to manually set the robot models for players if you intend to make playable robots.

Warning.pngWarning:Not setting this netprop back to true immediately after switching teams can cause many problems, including server crashes!
::ChangePlayerTeamMvM <- function(player, teamnum)
{
    local gamerules = Entities.FindByClassname(null, "tf_gamerules")
    NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", false)
    player.ForceChangeTeam(teamnum, false)
    NetProps.SetPropBool(gamerules, "m_bPlayingMannVsMachine", true)
}
ChangePlayerTeamMvM(GetListenServerHost(), Constants.ETFTeam.TF_TEAM_PVE_INVADERS) //Constants.ETFTeam.TF_TEAM_BLUE is also perfectly valid

Changing MvM Mission Names on the Scoreboard

You can change the displayed mission name on the scoreboard by changing the m_iszMvMPopfileName netprop on the tf_objective_resource entity.

Note.pngNote:This will always appear in uppercase on the scoreboard, as the game is expecting a file path to localize.
local resource = Entities.FindByClassname(null, "tf_objective_resource")
NetProps.SetPropString(resource, "m_iszMvMPopfileName", "New Mission Name Here")

Forcing a level change

The code snippet below will force an instant level change to the next map. The next level cannot be controlled unless the server has sv_allow_point_servercommand set to always, in which case the nextlevel convar can be changed to the desired level.

function ChangeLevel()
{	
	local intermission = SpawnEntityFromTable("point_intermission", {})
	Convars.SetValue("mp_chattime", 0)
	EntFireByHandle(intermission, "Activate", "", 0, null, null)
}

Getting the players active soundscape

If you would like to determine if a player is in a certain area of the map (notably if they are inside a building or not), and the map's soundscapes are configured correctly, you can check the players active soundscape whenever one is played to get a rough idea of where they are without needing to manually place trigger_multiples or using other tedious methods. Every soundscape has a unique index that can be found by typing soundscape_dumpclient in console. Note that being in developer mode by setting the developer console command to 1 or higher will print the actual name of the soundscape to console

::InSoundscapeIndex <- function(player, index)
{
    // printf("Soundscape Index: %d \t", GetPropInt(player, "m_Local.m_audio.soundscapeIndex")) //uncomment to print soundscape indexes to console, enable "developer 1" to match with the name of the soundscape
	if (GetPropInt(player, "m_Local.m_audio.soundscapeIndex") != index)
		return false
	else
		return true
}

//prints true for soundscape index 34 (Sawmill.Outside)
for (local soundscape; soundscape = Entities.FindByClassname(soundscape, "env_soundscape*"); )
{
	EntityOutputs.AddOutput(soundscape, "OnPlay", "!self", "RunScriptCode", "printl(InSoundscapeIndex(GetListenServerHost(), 34))", 0, -1)
}

Disabling Thriller taunt in Halloween

The following think function on a player will disable the Thriller taunt on Halloween mode and play the normal taunt instead.

function NoThrillerThink()
{
	if (self.IsTaunting())
	{
		for (local scene; scene = Entities.FindByClassname(scene, "instanced_scripted_scene");)
		{
			local owner = GetPropEntity(scene, "m_hOwner")
			if (owner == self)
			{
				local name = GetPropString(scene, "m_szInstanceFilename")
				local thriller_name = self.GetPlayerClass() == TF_CLASS_MEDIC ? "taunt07" : "taunt06"
				if (name.find(thriller_name) != null)
				{
					scene.Kill()
					self.RemoveCond(TF_COND_TAUNTING)
					self.Taunt(TAUNT_BASE_WEAPON, 0)
					break
				}
			}
		}
	}
	
	return -1
}

Applying paints to any entity

Cosmetic paints can be applied to any entity such as prop_dynamic by setting the owner entity to a tf_wearable (or any other econ entity).

local model_name = "models/player/items/demo/demo_ellis.mdl"
local origin = GetListenServerHost().GetOrigin()

local wearable = Entities.CreateByClassname("tf_wearable")
NetProps.SetPropBool(wearable, "m_AttributeManager.m_Item.m_bInitialized", true)
wearable.SetAbsOrigin(origin)
wearable.AddAttribute("set item tint RGB", 15132390, -1)
wearable.DispatchSpawn()
wearable.EnableDraw()

local prop = SpawnEntityFromTable("prop_dynamic",
{
    origin = origin,
    model = model_name
})
prop.SetOwner(wearable)

Error telemetry

It may be difficult to debug script errors when testing on multiplayer servers. The following code when executed sets up error telemetry, where all script errors are sent to a specific player by their matching SteamID3. Each script error will be printed to chat in red text, with full error information printed to console.

::TelemetrySteamID3 <- "[U:1:53275741]"

seterrorhandler(function(e)
{
	for (local player; player = Entities.FindByClassname(player, "player");)
	{
		if (NetProps.GetPropString(player, "m_szNetworkIDString") == TelemetrySteamID3)
		{
			local Chat = @(m) (printl(m), ClientPrint(player, 2, m))
			ClientPrint(player, 3, format("\x07FF0000AN ERROR HAS OCCURRED [%s].\nCheck console for details", e))
			
			Chat(format("\n====== TIMESTAMP: %g ======\nAN ERROR HAS OCCURRED [%s]", Time(), e))
			Chat("CALLSTACK")
			local s, l = 2
			while (s = getstackinfos(l++))
				Chat(format("*FUNCTION [%s()] %s line [%d]", s.func, s.src, s.line))
			Chat("LOCALS")
			if (s = getstackinfos(2))
			{
				foreach (n, v in s.locals) 
				{
					local t = type(v)
					t ==    "null" ? Chat(format("[%s] NULL"  , n))    :
					t == "integer" ? Chat(format("[%s] %d"    , n, v)) :
					t ==   "float" ? Chat(format("[%s] %.14g" , n, v)) :
					t ==  "string" ? Chat(format("[%s] \"%s\"", n, v)) :
									 Chat(format("[%s] %s %s" , n, t, v.tostring()))
				}
			}
			return
		}
	}
})

Readying up teams for mp_tournament 1 in offline

Gamemodes for Casual must be tested with mp_tournament 1 and mp_tournament_stopwatch 0 to be considered for inclusion. However this usually requires 2 real players to ready up for their team, which may be undesirable for offline testing. As a workaround, the following code snippet can be executed to pick any player from both teams and spoof readying up.

local GameRules = Entities.FindByClassname(null, "tf_gamerules")
local MAX_CLIENTS = MaxClients().tointeger()

// 0 - not ready
// 1 - ready
local ready_state = 1

local red_check = false
local blue_check = false

for (local i = 1; i <= MAX_CLIENTS; i++)
{
	local player = PlayerInstanceFromIndex(i)
	if (!player)
		continue
		
	local team = player.GetTeam()
	if (!(team & 2))
		continue
	
	if (team == 2)
	{
		if (red_check)
			continue
		red_check = true
	}
	else if (team == 3)
	{
		if (blue_check)
			continue
		blue_check = true
	}
	
	if (NetProps.GetPropBoolArray(GameRules, "m_bTeamReady", team))
		continue
	
	NetProps.SetPropBoolArray(GameRules, "m_bTeamReady", ready_state == 1, team)
	
	SendGlobalGameEvent("tournament_stateupdate",
	{
		userid = player.entindex(),
		readystate = ready_state,
		namechange = 0,
		oldname = " ",
		newname = " ",
		
	})

	if (ready_state == 0)
	{
		NetProps.SetPropFloat(GameRules, "m_flRestartRoundTime", -1.0)
		NetProps.SetPropBool(GameRules, "m_bAwaitingReadyRestart", true)
	}
}

Placing Sapper on a Building

Placing a sapper on an entity isn't supposed to be done by manual means, however it can be spoofed by selecting a random player and pretending they "sapped" the building, and reverting everything on the same frame which makes it look seamless.

Warning.pngWarning:This has not been tested too much. There could be more edge cases where a player can't place a Sapper which need to be resolved.
Todo: The weapon is holstered each time, fixing this requires manually spoofing Weapon_Switch.
const MAX_WEAPONS = 8

function SapBuilding(building)
{
	// select first player found
	local player = Entities.FindByClassname(null, "player")
	if (!player)
		return
		
	// find existing builder weapon if applicable
	local player_class = player.GetPlayerClass()
	local old_builder, old_slot
	if (player_class == 8 || player_class == 9) // TF_CLASS_SPY or TF_CLASS_ENGINEER
	{
		for (local i = 0; i < MAX_WEAPONS; i++)
		{
			local held_weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
			if (held_weapon == null)
				continue
			if (held_weapon.GetClassname() != "tf_weapon_builder")
				continue

			old_builder = held_weapon
			old_slot = i
			NetProps.SetPropEntityArray(player, "m_hMyWeapons", null, i)
			break
		}
	}
	
	local old_lifestate = NetProps.GetPropInt(player, "m_lifeState")
	local old_team = player.GetTeam()
	
	local building_team = building.GetTeam()
	local enemy_team
	if (building_team == 2) // red
		enemy_team = 3
	else if (building_team == 3) // blue
		enemy_team = 2
	else
		enemy_team = 2
	
	// spoof being alive and on opposite team
	NetProps.SetPropInt(player, "m_lifeState", 0)
	NetProps.SetPropInt(player, "m_iTeamNum", enemy_team)
		
	// give sapper weapon
    local weapon = Entities.CreateByClassname("tf_weapon_builder")
    NetProps.SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", 735)
    NetProps.SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true)
    NetProps.SetPropBool(weapon, "m_bValidatedAttachedEntity", true)
    weapon.SetTeam(enemy_team)
    weapon.DispatchSpawn()
	NetProps.SetPropInt(weapon, "m_iObjectType", 3)
	NetProps.SetPropInt(weapon, "m_iSubType", 3)
	NetProps.SetPropIntArray(weapon, "m_aBuildableObjectTypes", 1, 3)
	
	player.Weapon_Equip(weapon)

	// teleport player to the building and hold the sapper
	local old_origin = player.GetOrigin()
	local old_angles = player.LocalEyeAngles()
	local old_weapon = player.GetActiveWeapon()
	
	player.SetAbsOrigin(building.GetOrigin() - Vector(64, 0, 0))
	NetProps.SetPropVector(player, "pl.v_angle", Vector())
	player.Weapon_Switch(weapon)
	
	weapon.PrimaryAttack()
	
	// remove owner from the sapper that was just placed
	for (local sapper; sapper = Entities.FindByClassname(sapper, "obj_attachment_sapper");)
	{
		if (NetProps.GetPropEntity(sapper, "m_hBuiltOnEntity") == building)
		{
			NetProps.SetPropEntity(sapper, "m_hBuilder", null)
			break
		}
	}
		
	// revert all changes
	if (old_builder)
		NetProps.SetPropEntityArray(player, "m_hMyWeapons", old_builder, old_slot)
	
	NetProps.SetPropInt(player, "m_lifeState", old_lifestate)
	NetProps.SetPropInt(player, "m_iTeamNum", old_team)
	
	player.Weapon_Switch(old_weapon)
	NetProps.SetPropVector(player, "pl.v_angle", old_angles + Vector())
	player.SetAbsOrigin(old_origin)
	
	weapon.Destroy()
}

// example
local building = Entities.FindByClassname(null, "obj_dispenser")
if (building)
{
	if (!NetProps.GetPropBool(building, "m_bHasSapper"))
		SapBuilding(building)
}

Creating Bots That Use the Navmesh

Warning.pngWarning:This was made before NextBot functions were exposed to VScript. Most of the code here is now unnecessary

The following code show an example of creating a primitive bot that navigates using the navmesh. When this script is executed, a bot will be spawned at the host player's crosshair. This bot will simply follow the player and path-find along the map layout. The code is heavily commented, and debug visualizations are available to see the path-finding algorithm in action.

Warning.pngWarning:Making bots is not an easy task. This code shows how much work is required just to setup a simple bot!
Icon-Bug.pngBug:The pathfinding logic skips the first point  [todo tested in ?]
Icon-Bug.pngBug:This does not work when executed from the scope of an entity such as logic_script  [todo tested in ?]
// Constrains an angle into [-180, 180] range
function NormalizeAngle(target)
{
	target %= 360.0
	if (target > 180.0)
		target -= 360.0
	else if (target < -180.0)
		target += 360.0
	return target
}

// Approaches an angle at a given speed
function ApproachAngle(target, value, speed)
{
	target = NormalizeAngle(target)
	value = NormalizeAngle(value)
	local delta = NormalizeAngle(target - value)
	if (delta > speed)
		return value + speed
	else if (delta < -speed)
		return value - speed
	return value
}

// Converts a vector direction into angles
function VectorAngles(forward)
{
	local yaw, pitch
	if ( forward.y == 0.0 && forward.x == 0.0 )
	{
		yaw = 0.0
		if (forward.z > 0.0)
			pitch = 270.0
		else
			pitch = 90.0
	}
	else
	{
		yaw = (atan2(forward.y, forward.x) * 180.0 / Constants.Math.Pi)
		if (yaw < 0.0)
			yaw += 360.0
		pitch = (atan2(-forward.z, forward.Length2D()) * 180.0 / Constants.Math.Pi)
		if (pitch < 0.0)
			pitch += 360.0
	}

	return QAngle(pitch, yaw, 0.0)
}

// Coordinate which is part of a path
class PathPoint
{
	constructor(_area, _pos, _how)
	{
		area = _area
		pos = _pos
		how = _how
	}

	area = null		// Which area does this point belong to?
	pos = null		// Coordinates of the point
	how = null		// Type of traversal. See Constants.ENavTraverseType
}

// The big boy that handles all our behavior
class Bot
{
	function constructor(bot_ent, follow_ent)
	{
		bot = bot_ent

		move_speed = 230.0
		turn_rate = 5.0
		search_dist_z = 128.0
		search_dist_nearest = 128.0

		path = []
		path_index = 0
		path_reach_dist = 16.0
		path_follow_ent = follow_ent
		path_follow_ent_dist = 50.0
		path_target_pos = follow_ent.GetOrigin()
		path_update_time_next = Time()
		path_update_time_delay = 0.2
		path_update_force = true
		area_list = {}

		seq_idle = bot_ent.LookupSequence("Stand_MELEE")
		seq_run = bot_ent.LookupSequence("Run_MELEE")
		pose_move_x = bot_ent.LookupPoseParameter("move_x")

		debug = true

		// Add behavior that will run every tick
		AddThinkToEnt(bot_ent, "BotThink")
	}

	function UpdatePath()
	{
		// Clear out the path first
		ResetPath()

		// If there is a follow entity specified, then the bot will pathfind to the entity
		if (path_follow_ent && path_follow_ent.IsValid())
			path_target_pos = path_follow_ent.GetOrigin()

		// Pathfind from the bot's position to the target position
		local pos_start = bot.GetOrigin()
		local pos_end = path_target_pos

		local area_start = NavMesh.GetNavArea(pos_start, search_dist_z)
		local area_end = NavMesh.GetNavArea(pos_end, search_dist_z)

		// If either area was not found, try use the closest one
		if (area_start == null)
			area_start = NavMesh.GetNearestNavArea(pos_start, search_dist_nearest, false, true)
		if (area_end == null)
			area_end = NavMesh.GetNearestNavArea(pos_end, search_dist_nearest, false, true)

		// If either area is still missing, then bot can't progress
		if (area_start == null || area_end == null)
			return false

		// If the start and end area is the same, one path point is enough and all the expensive path building can be skipped
		if (area_start == area_end)
		{
			path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES))
			return true
		}

		// Build list of areas required to get from the start to the end
		if (!NavMesh.GetNavAreasFromBuildPath(area_start, area_end, pos_end, 0.0, Constants.ETFTeam.TEAM_ANY, false, area_list))
			return false

		// No areas found? Uh oh
		if (area_list.len() == 0)
			return false

		// Now build points using the list of areas, which the bot will then follow
		local area_target = area_list["area0"]
		local area = area_target
		local area_count = area_list.len()

		// Iterate through the list of areas in order and initialize points
		for (local i = 0; i < area_count && area != null; i++)
		{
			path.append(PathPoint(area, area.GetCenter(), area.GetParentHow()))
			area = area.GetParent(); // Advances to the next connected area
		}

		// Reverse the list of path points as the area list is connected backwards
		path.reverse()

		// Now compute accurate path points, using adjacent points + direction data from nav
		local path_first = path[0]
		local path_count = path.len()

		// First point is simply our current position
		path_first.pos = bot.GetOrigin()
		path_first.how = Constants.ENavTraverseType.NUM_TRAVERSE_TYPES // No direction specified

		for (local i = 1; i < path_count; i++)
		{
			local path_from = path[i - 1]
			local path_to = path[i]

			// Computes closest point within the "portal" between adjacent areas
			path_to.pos = path_from.area.ComputeClosestPointInPortal(path_to.area, path_to.how, path_from.pos)
		}

		// Add a final point so the bot can precisely move towards the end point when it reaches the final area
		path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES))
	}

	function AdvancePath()
	{
		// Check for valid path first
		local path_len = path.len()
		if (path_len == 0)
			return false

		local path_pos = path[path_index].pos
		local bot_pos = bot.GetOrigin()

		// Are we close enough to the path point to consider it as 'reached'?
		if ((path_pos - bot_pos).Length2D() < path_reach_dist)
		{
			// Start moving to the next point
			path_index++
			if (path_index >= path_len)
			{
				// End of the line!
				ResetPath()
				return false
			}
		}

		return true
	}

	function ResetPath()
	{
		area_list.clear()
		path.clear()
		path_index = 0
	}

	function Move()
	{
		// Recompute the path if forced to do so
		if (path_update_force)
		{
			UpdatePath()
			path_update_force = false
		}
		// Recompute path to our target if present
		else if (path_follow_ent && path_follow_ent.IsValid())
		{
			// Is it time to re-compute the path?
			local time = Time()
			if (path_update_time_next < time)
			{
				// Check if target has moved far away enough
				if ((path_target_pos - path_follow_ent.GetOrigin()).Length() > path_follow_ent_dist)
				{
					UpdatePath()
					// Don't recompute again for a moment
					path_update_time_next = time + path_update_time_delay
				}
			}
		}

		// Check and advance up our path
		if (AdvancePath())
		{
			local path_pos = path[path_index].pos
			local bot_pos = bot.GetOrigin()

			// Direction towards path point
			local move_dir = (path_pos - bot_pos)
			move_dir.Norm()

			// Convert direction into angle form
			local move_ang = VectorAngles(move_dir)

			// Approach new desired angle but only on the Y axis
			local bot_ang = bot.GetAbsAngles()
			move_ang.x = bot_ang.x
			move_ang.y = ApproachAngle(move_ang.y, bot_ang.y, turn_rate)
			move_ang.z = bot_ang.z

			// Set our new position and angles
			// Velocity is calculated from direction times speed, and converted from per-second to per-tick time
			bot.SetAbsOrigin(bot_pos + (move_dir * move_speed * FrameTime()))
			bot.SetAbsAngles(move_ang)

			return true
		}

		return false
	}

	function Update()
	{
		// Try moving
		if (Move())
		{
			// Moving, set the run animation
			if (bot.GetSequence() != seq_run)
			{
				bot.SetSequence(seq_run)
				bot.SetPoseParameter(pose_move_x, 1.0) // Set the move_x pose to max weight
			}
		}
		else
		{
			// Not moving, set the idle animation
			if (bot.GetSequence() != seq_idle)
			{
				bot.SetSequence(seq_idle)
				bot.SetPoseParameter(pose_move_x, 0.0) // Clear the move_x pose
			}
		}

		// Replay animation if it has finished
		if (bot.GetCycle() > 0.99)
			bot.SetCycle(0.0)

		// Run animations
		bot.StudioFrameAdvance()
		bot.DispatchAnimEvents(bot)

		// Visualize current path in debug mode
		if (debug)
		{
			// Stay around for 1 tick
			// Debugoverlays are created on 1st tick but start rendering on 2nd tick, hence this must be doubled
			local frame_time = FrameTime() * 2.0

			// Draw connected path points
			local path_len = path.len()
			if (path_len > 0)
			{
				local path_start_index = path_index
				if (path_start_index == 0)
					path_start_index++

				for (local i = path_start_index; i < path_len; i++)
				{
					DebugDrawLine(path[i - 1].pos, path[i].pos, 0, 255, 0, true, frame_time)
				}
			}

			// Draw areas from built path
			foreach (name, area in area_list)
			{
				area.DebugDrawFilled(255, 0, 0, 30, frame_time, true, 0.0)
				DebugDrawText(area.GetCenter(), name, false, frame_time)
			}
		}

		return 0.0 // Think again next frame
	}

	function OnKilled()
	{
		// Change life state to "dying"
		// The bot won't take any more damage, and sentries will stop targeting it
		NetProps.SetPropInt(bot, "m_lifeState", 1)
		// Reset health, preventing the default base_boss death behavior
		bot.SetHealth(bot.GetMaxHealth() * 20)
		// Custom death behavior can be added here
		// For this example, turn into a ragdoll with the saved damage force
		bot.BecomeRagdollOnClient(damage_force)
	}

	bot = null						// The bot entity we belong to

	move_speed = null				// How fast to move
	turn_rate = null				// How fast to turn
	search_dist_z = null			// Maximum distance to look for a nav area downwards
	search_dist_nearest = null 		// Maximum distance to look for any nearby nav area

	path = null						// List of BotPathPoints
	path_index = null				// Current path point bot is at, -1 if none
	path_reach_dist = null			// Distance to a path point to be considered as 'reached'
	path_follow_ent = null			// What entity to move towards
	path_follow_ent_dist = null		// Maximum distance after which the path is recomputed
									// if follow entity's current position is too far from our target position
	path_target_pos = null			// Position where bot wants to navigate to
	path_update_time_next = null	// Timer for when to update path again
	path_update_time_delay = null   // Seconds to wait before trying to attempt to update path again
	path_update_force = null		// Force path recomputation on the next tick
	area_list = null				// List of areas built in path

	seq_idle = null					// Animation to use when idle
	seq_run = null					// Animation to use when running
	pose_move_x = null				// Pose parameter to set for running animation

	damage_force = null				// Damage force from the bot's last OnTakeDamage event

	debug = null					// When true, debug visualization is enabled

}

function BotThink()
{
	// Let the bot class handle all the work
	return self.GetScriptScope().my_bot.Update()
}

function BotCreate()
{
	// Find point where player is looking
	local player = GetListenServerHost()
	local trace =
	{
		start = player.EyePosition(),
		end = player.EyePosition() + (player.EyeAngles().Forward() * 32768.0),
		ignore = player
	}

	if (!TraceLineEx(trace))
	{
		printl("Invalid bot spawn location")
		return null
	}

	// Spawn bot at the end point
	local bot = SpawnEntityFromTable("base_boss",
	{
		targetname = "bot",
		origin = trace.pos,
		model = "models/bots/heavy/bot_heavy.mdl",
		playbackrate = 1.0, // Required for animations to be simulated
		health = 300
	})

	// Add scope to the entity
	bot.ValidateScriptScope()
	// Append custom bot class and initialize its behavior
	bot.GetScriptScope().my_bot <- Bot(bot, player)

        // Fix the default step height which is too high
        EntFireByHandle(bot, "SetStepHeight", "18", 0, null, null)

	return bot
}

function OnScriptHook_OnTakeDamage(params)
{
	local ent = params.const_entity
	local inf = params.inflictor
    if (ent.IsPlayer() && HasBotScript(inf) && params.damage_type == 1)
    {
		// Don't crush the player if a bot pushes them into a wall
        params.damage = 0
    }
	if (ent.GetClassname() == "base_boss" && HasBotScript(ent))
	{
		// Save the damage force into the bot's data
		ent.GetScriptScope().my_bot.damage_force = params.damage_force
	}
}

function OnGameEvent_npc_hurt(params)
{
	local ent = EntIndexToHScript(params.entindex)
	if (HasBotScript(ent))
	{
		// Check if a bot is about to die
		if ((ent.GetHealth() - params.damageamount) <= 0)
		{
			// Run the bot's OnKilled function
			ent.GetScriptScope().my_bot.OnKilled()
		}
	}
}

function HasBotScript(ent)
{
	// Return true if this entity has the my_bot script scope
	return (ent.GetScriptScope() != null && ent.GetScriptScope().my_bot != null)
}

__CollectGameEventCallbacks(this)
BotCreate()

See also