Creating custom weapons - VScript: Difference between revisions
Created page with "{{stub}} category:guides" |
mNo edit summary |
||
| (3 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
{{stub}} | {{stub}} | ||
{{todo|Make the page more beginner friendly: | |||
Start simple | |||
Give plenty of examples that each go through multiple new things | |||
Add explanations for detecting actions like attack, attack2 and reload | |||
}} | |||
TF2C extends TF2 and MapBase's VScripting, making anything possible in either of them mostly possible in TF2C. | |||
VScript in TF2C uses [https://en.wikipedia.org/wiki/Squirrel_(programming_language) Squirrel] <sup>{{tooltip|Note|as of writing the actual squirrel website is inaccessible for whatever reason, so the Wikipedia page will have to do for now I suppose}}</sup>, which is also used by TF2 and MapBase for their VScripting as well. | |||
= Hooks = | |||
A hook in modding is when you get the game to run a function, so you can perform custom behavior or replace existing behavior. We can't actually replace TF2C's code directly of course, so instead we use a tool provided to us by Valve and the TF2C team called VScript.<br> | |||
Hooking can be done in two ways, a table and collector (making a table of functions and collecting them) and a direct hook to the game's events. As of writing, the writer here has no idea what the difference is. | |||
== Table and collector method == | |||
Here's an example table that disables Medic's health regen when he's holding a weapon that has a custom, schema defined attribute. | |||
<pre> | |||
::AttributeTable <- { | |||
// when a player heals | |||
function OnGameEvent_player_healed(params) | |||
{ | |||
if (params.patient == params.healer) // if we heal ourselves | |||
{ | |||
// get ourselves | |||
local player = GetPlayerFromUserID(params.patient) | |||
local activeweapon = player.GetActiveWeapon() | |||
// check the weapons attributes and "disable" healing if they have a specific attribute | |||
if ( activeweapon.GetAttribute("medic no heal", 0) == 1 ) | |||
{ | |||
if (player.GetPlayerClass() == Constants.ETFClass.TF_CLASS_MEDIC) | |||
{ | |||
// unheal... basically | |||
player.SetHealth(player.GetHealth() - params.amount) | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</pre> | |||
And later we can do <code>__CollectGameEventCallbacks(AttributeTable)</code> which collects all of our functions in the table and acts on them.<br> | |||
While there is no such thing as a "medic no heal" attribute, we can just make one up in our item schema like this: | |||
<pre> | |||
"items_game" | |||
{ | |||
<...> | |||
"attributes" | |||
{ | |||
"7000" | |||
{ | |||
"name" "medic no heal" | |||
"description_string" "While Active: No longer passively heal" | |||
"description_format" "value_is_additive" | |||
"hidden" "0" | |||
"effect_type" "negative" | |||
} | |||
} | |||
} | |||
</pre> | |||
It also doesn't need an attribute class either, since the Vscript only uses the attribute's name, so ideally you shouldn't have an attribute class on vscript attributes. | |||
== Direct hook method == | |||
The direct hook method is the same way the official April Fools weapon pack does its VScript attributes <sub>{{tooltip|Todo|should we link this?}}</sub> | |||
For example we're going to pick apart one of them, specifically the "mod shoot self" attribute. | |||
<pre> | |||
::ListenToGameEvent( "player_shoot", function( hEvent ) | |||
{ | |||
// Get the player and throw away the event if the event isn't from the player | |||
local hPlayer = GetPlayerFromUserID( hEvent.userid ); | |||
if ( !hPlayer ) | |||
{ | |||
return; | |||
} | |||
// Get the weapon and throw away the event if the event isn't from the weapon | |||
local hWeapon = hPlayer.GetActiveWeapon() | |||
if ( !hWeapon || !hWeapon.GetAttribute( "mod shoot self", 0 ) ) | |||
{ | |||
return; | |||
} | |||
// Take damage equal to your health, plus 10 | |||
hPlayer.TakeDamage( hPlayer.GetMaxHealth() + 10, Constants.FDmgType.DMG_PREVENT_PHYSICS_FORCE, hPlayer ); | |||
// For the sound to not get cut off from dying we need to emit it from the ragdoll. | |||
local hRagdoll = NetProps.GetPropEntity( hPlayer, "m_hRagdoll" ); | |||
if ( hRagdoll ) | |||
{ | |||
hRagdoll.EmitSound( "Weapon_Scatter_Gun.Single" ); | |||
} | |||
// Turn the player's head around so the shots are fired behind him. | |||
local angEyeAngles = hPlayer.EyeAngles() | |||
angEyeAngles.x = -angEyeAngles.x; | |||
angEyeAngles.y += 180; | |||
if ( angEyeAngles.y > 180 ) | |||
{ | |||
angEyeAngles.y -= 360; | |||
} | |||
hPlayer.SnapEyeAngles( angEyeAngles ); | |||
}, "mod_shoot_self" ); | |||
</pre> | |||
We can actually easily condense the whole script into one line so it's easier to understand how the hook works and how it modifies the game behavior.<br> | |||
<code>::ListenToGameEvent( <GameEvent>, function(args){behavior}, <String>)</code><br> | |||
Now that we have it all condensed down into one line, this is basically how hooking a game event works. We run a function when a game event happens, which passes a table of information through args, and runs behavior. | |||
For a much simpler example, here's a one-line piece of code that prints to the log whenever a gun is fired. <code>::ListenToGameEvent( "player_shoot", function(event){printl("doing things")}, "didathing")</code> In this instance, the table of info is passed to "event", which isn't used but is still required because the game still calls the function with that table regardless, and can't just throw it out. | |||
will print "doing things" to the console whenever we shoot. | |||
= See also = | |||
[https://developer.valvesoftware.com/wiki/Team_Fortress_2/Docs/Scripting/Script_Functions TF2's VScript Functions]<br> | |||
[https://developer.valvesoftware.com/wiki/Team_Fortress_2/Docs/Scripting/Game_Events TF2's Game Events (used for hooks)]<br> | |||
[https://developer.valvesoftware.com/wiki/Mapbase/Scripting/Script_Functions MapBase's VScript Functions]<br> | |||
[https://developer.valvesoftware.com/wiki/Team_Fortress_2/Docs/Scripting/Script_Functions MapBase's Game Events (used for hooks)]<br> | |||
[https://developer.valvesoftware.com/wiki/Source_2013_MP/Scripting/Game_Events Source 2013's Game Events (used for hooks)]<br> | |||
[[category:guides]] | [[category:guides]] | ||
Latest revision as of 15:56, 16 July 2026
| This article is a stub. You can help TF2 Classified Wiki by expanding it. |
| A todo list has been written for this article.
The specific instructions are: Make the page more beginner friendly: Start simple Give plenty of examples that each go through multiple new things
|
TF2C extends TF2 and MapBase's VScripting, making anything possible in either of them mostly possible in TF2C.
VScript in TF2C uses Squirrel Note, which is also used by TF2 and MapBase for their VScripting as well.
Hooks
A hook in modding is when you get the game to run a function, so you can perform custom behavior or replace existing behavior. We can't actually replace TF2C's code directly of course, so instead we use a tool provided to us by Valve and the TF2C team called VScript.
Hooking can be done in two ways, a table and collector (making a table of functions and collecting them) and a direct hook to the game's events. As of writing, the writer here has no idea what the difference is.
Table and collector method
Here's an example table that disables Medic's health regen when he's holding a weapon that has a custom, schema defined attribute.
::AttributeTable <- {
// when a player heals
function OnGameEvent_player_healed(params)
{
if (params.patient == params.healer) // if we heal ourselves
{
// get ourselves
local player = GetPlayerFromUserID(params.patient)
local activeweapon = player.GetActiveWeapon()
// check the weapons attributes and "disable" healing if they have a specific attribute
if ( activeweapon.GetAttribute("medic no heal", 0) == 1 )
{
if (player.GetPlayerClass() == Constants.ETFClass.TF_CLASS_MEDIC)
{
// unheal... basically
player.SetHealth(player.GetHealth() - params.amount)
}
}
}
}
}
And later we can do __CollectGameEventCallbacks(AttributeTable) which collects all of our functions in the table and acts on them.
While there is no such thing as a "medic no heal" attribute, we can just make one up in our item schema like this:
"items_game"
{
<...>
"attributes"
{
"7000"
{
"name" "medic no heal"
"description_string" "While Active: No longer passively heal"
"description_format" "value_is_additive"
"hidden" "0"
"effect_type" "negative"
}
}
}
It also doesn't need an attribute class either, since the Vscript only uses the attribute's name, so ideally you shouldn't have an attribute class on vscript attributes.
Direct hook method
The direct hook method is the same way the official April Fools weapon pack does its VScript attributes Todo For example we're going to pick apart one of them, specifically the "mod shoot self" attribute.
::ListenToGameEvent( "player_shoot", function( hEvent )
{
// Get the player and throw away the event if the event isn't from the player
local hPlayer = GetPlayerFromUserID( hEvent.userid );
if ( !hPlayer )
{
return;
}
// Get the weapon and throw away the event if the event isn't from the weapon
local hWeapon = hPlayer.GetActiveWeapon()
if ( !hWeapon || !hWeapon.GetAttribute( "mod shoot self", 0 ) )
{
return;
}
// Take damage equal to your health, plus 10
hPlayer.TakeDamage( hPlayer.GetMaxHealth() + 10, Constants.FDmgType.DMG_PREVENT_PHYSICS_FORCE, hPlayer );
// For the sound to not get cut off from dying we need to emit it from the ragdoll.
local hRagdoll = NetProps.GetPropEntity( hPlayer, "m_hRagdoll" );
if ( hRagdoll )
{
hRagdoll.EmitSound( "Weapon_Scatter_Gun.Single" );
}
// Turn the player's head around so the shots are fired behind him.
local angEyeAngles = hPlayer.EyeAngles()
angEyeAngles.x = -angEyeAngles.x;
angEyeAngles.y += 180;
if ( angEyeAngles.y > 180 )
{
angEyeAngles.y -= 360;
}
hPlayer.SnapEyeAngles( angEyeAngles );
}, "mod_shoot_self" );
We can actually easily condense the whole script into one line so it's easier to understand how the hook works and how it modifies the game behavior.
::ListenToGameEvent( <GameEvent>, function(args){behavior}, <String>)
Now that we have it all condensed down into one line, this is basically how hooking a game event works. We run a function when a game event happens, which passes a table of information through args, and runs behavior.
For a much simpler example, here's a one-line piece of code that prints to the log whenever a gun is fired. ::ListenToGameEvent( "player_shoot", function(event){printl("doing things")}, "didathing") In this instance, the table of info is passed to "event", which isn't used but is still required because the game still calls the function with that table regardless, and can't just throw it out.
will print "doing things" to the console whenever we shoot.
See also
TF2's VScript Functions
TF2's Game Events (used for hooks)
MapBase's VScript Functions
MapBase's Game Events (used for hooks)