Left 4 Dead 2

Left 4 Dead 2

111 ratings
Custom Scripted Weapons
By Rectus
This guide describes how to create a custom weapon with new functionality, using a control vscript and a custom melee weapon as a base.
   
Award
Favorite
Favorited
Unfavorite
Introduction
The guide assumes general L4D2 modding experience, knowledge in creating custom weapons, and some basic familiarity with scripting and the Squirrel language.

Please ask in the comments if you have any questions.

Template Files
The example and template files are available in this workshop item. Use GCFScape[nemesis.thewavelength.net] to browse and extract the files.
https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=638790000

How it works
Left 4 Dead does not allow you to create custom weapons, with one exception; melee weapons. All melee weapons are based of the same entity, with a melee weapon script defining the individual weapon behavior. New melee weapons can be created by simply creating a new script for them, and including it in the mission script for each campaign that the weapon should spawn in.

The 2013 EMS update added various new scripting features for the VScript system, including the ability to spawn new entities, get callbacks on game events and monitor player key presses.

Using these features, new weapons (and other kinds of items) can be created.

https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=612827551

https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=1077649911

https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=923209945

Collection of all my custom weapons:
https://steamhost.cn/steamcommunity_com/workshop/filedetails/?id=923565977

Limitations and caveats

The functionality of the scripted weapon is limited to what can be done with VScript functions and what entities that can be spawned in during gameplay.

In particular damaging enemies and attributing damage to the user works badly. There is no way to damage enemies directly, so damage has to be done though entities like point_hurt or script_trigger_hurt. These entities don't count the user to be the damage source so they don't alert witches or add to friendly fire counters.There now is a TakeDamage() method for entities, although it does not support positional damage.

The First Person viewmodels have their animations limited to how melee weapons animate, so no reload animations or related ones are possible. There is no way to modify the viewmodels body groups or dynamically either. Skins can be set, but they get reset quickly, causing flickering. It is also possible to use the NetProp functions to swap out the viewmodel on the fly.

Detecting user button presses is limited to polling once every 0.1 second frame, so there can be a firing delay and quick taps to buttons may go unnoticed. This makes it difficult to do precision weapons well, so it is recommended to make weapons that encourage players to hold down the fire button instead.

Some spaecial effects like bullet holes are not possible(at least I haven't found any way), and other effects require spawning multiple entities for them.

Useful features
  • Entity Spawning - Most networked point based entities can be spawned in by the scripting system, and some brush based ones can be converted to simple point based ones by the EMS entity group exporter.
  • Ray tracing - Allows you to do hitscan weapon behavior, using the TraceLine() function.
  • Particle effects - Can be added with entities to display many kinds of effects, including bullet tracers.
Creating the Melee Weapon
The first part we need is a custom melee weapon to track.

Melee Weapon Scripts
A Melee weapon is defined by adding a melee weapon script under the \scripts\melee\ directory. Copy and modify one of the official scripts or the one for the provided template weapon. The file name of the script file determines the internal weapon name, so for example a weapon with the script \scripts\melee\my_weapon.txt will be called my_weapon.

The template weapon script is called template_weapon.txt

The important thing to set in the script is the viewmodel value. The weapon control script tracks weapons by their viewmodel, so it must be unique from other weapons and props. In the example template_weapon script the viewmodel is set to a recompiled version of the baseball bat model.

It does not seem possible to completely disable the melee weapon functionality, but you can set both the "starttime" and "endtime" values of each attack animation to minimize the time the attack is performed, and set both "startdir" and "enddir" to "N" to aim the attack away.

Mission Scripts
To make the weapons spawn in a campaign or map, they need to be added to it's mission script. For the official campaigns, the mission script for each campaign will need to be overridden. Extract the scripts from the game .vpks or use the provided templates.

Only the meleeweapons setting needs to be modified, adding the weapon's internal name to the semicolon separated list.
"meleeweapons" "fireaxe;crowbar;cricket_bat;katana;baseball_bat;knife;my_weapon"

Notes:
This will make the mod incompatible with others that modify the mission scripts, like melee weapon unlockers.
For custom campaigns the mission script cannot be overridden cleanly, so it is only advisable to include support for the weapon directly in the campaign addon.
Getting the Custom Weapon Scripts to run
The custom weapons are tracked by the control script custom_weapon_controller.nut that is included in the template files.

The controller can be loaded in with a few lines of script:
::g_WeaponController <- {} DoIncludeScript("custom_weapon_controller", g_WeaponController) if(g_WeaponController.AddCustomWeapon("models/weapons/melee/v_my_weapon.mdl", "my_weapon_script")) { g_WeaponController.SetReplacePercentage("my_weapon", 15) }

The first two lines loads in the controller script.

The next line tells the controller to start tracking a weapon. It needs the viewmodel file to track and the custom weapon VScript (more on that next chapter). The custom weapon script used for the template is called custom_template.nut. The if clause is only needed if the replacement function is used.
g_WeaponController.AddCustomWeapon(string viewModel, string script)

The function checks if the custom weapon script can be loaded, so other weapons not included in the mod can be added to provide intercompatibility.

The last line is an optional feature to replace a portion of the melee weapon spawns with the custom weapon.
g_WeaponController.SetReplacePercentage(string weapon, int percentage)
It takes the internal name of the weapon as well as a percentage of weapons to replace.

Official Campaigns
There is no official way of loading scripts work in to every campaign, so we are going to need to override a sytem script. The best one to modify is scriptedmode.nut. In the ScriptMode_Init() function, add the code to load the controller after line 74 where the sm_spawn script has been loaded, or use and modify the included template.

Note:
This makes the mod incompatible with a variety of other mods.

Custom Campaigns
If you are authoring a custom map/campaign, this fairly easy as well. Create a logic_script entity and add a script name to it's Entity Vscripts keyvalue, for example mymap_custom_weapon_loader. Add the code from the wepon controller to the script, along with the setup code above. An example from the Diescraper campaign is avaliable here: https://github.com/Rectus/diescraper_sources/blob/master/scripts/vscripts/diescraper_custom_weapon_controller.nut
The Custom Weapon Script
Now we come to the part your all here for, adding all the cool features to the weapon.

Open the template script custom_template.nut and save it in the vscripts directory, for example as my_weapon_script.nut

The Custom Weapon VScript is an Entity Script that gets attached to the custom weapon_melee entity by the controller when a player pick it up for the first time.

Like all entity scripts, the handle of the backing entity can be accessed through the self variable. When a player has the weapon equipped in hand, the player and viewmodel can be accessed through the currentPlayer and viewmodel variables that are set from the OnEquipped() callback function.

Callback Functions
The controller makes callbacks to functions in the script, allowing the script to react to game events like the player equipping the weapon and firing.

  • OnInitialize() - Called after the script has loaded
    Put any initialization code here, for example precaching assets and spawning persistent entities.
    Unlike normally loading entity scripts, the variables and functions are not fully available until the script has loaded completely, so it's easier to put initialization code here than directly in the script scope.
  • OnEquipped(handle player, handle viewmodel) - Called when player switches to the weapon
    Supplies handles to the player and viewmodel. In the template these are copied to script scope variables.
  • OnUnEquipped() - Called when the player puts away the weapon or drops it
  • OnStartFiring() - Called when the player starts firing the weapon
  • OnFireTick(int playerButtonMask) - Called every 0.1 seconds while the player is firing
    The argument is the result of CTerrorPlayer::GetButtonMask()
  • OnEndFiring() - Called when the player releases the fire button
  • OnAmmoRefilled() - Called when the player picks up ammunition

Entity Tracking
Since there is no reliable way to run code on the weapon when it is killed, the custom weapon controller can track and clean up entites that the weapon script has spawned. To do that, each tracked entity has to be registered with the weapon controller.

  • weaponController.RegisterTrackedEntity(entity, weapon) - Register an entity to the specified weapon entity (self if the wepon script).
  • weaponController.UnregisterTrackedEntity(entity, weapon) - Unregister an entity.

Examples
The template weapon script contains a basic usage example, a magic staff that shoots explosions.

The Explosion
The explosion consists of an env_explosion entity for damage and effects, and a separate sound played through the script.

The env_explosion entity is persistent through the weapons life. It is spawned in OnInitialize() through the g_ModeScript.CreateSingleSimpleEntityFromTable() utility function, from a keyvlaue table at the end of the script. The handle of the entity is stored in the script scope. Note that the keyvalues in the table after set up the same way is in Hammer, with the exception of vectors and angles. The classname key defines the entity type.

Note that if you want to spawn models, they need to be precahced first with the self.PrecacheModel(string modelName) method.

The explosion is triggered from the ShootFireBall() function, by first tracing where the player is looking, moving the explosion entity to the end of the trace using the CBaseEntity::SetOrigin() method, and then triggering the explosion with an entity output using the DoEntFire() function.

Playing Sounds
The explosion sound is played on the explosion entity using the EmitSoundOn() function. In addition, a looping heartbeat sound is played the same way when the weapon is equipped, and stopped when unequipped using the StopSoundOn() function.

Sounds can also be played for specific players using the EmitsoundOnClient(string soundScript, handle player) function.

Note that sounds have to be precahed before they are played. This can be done in the OnInitialize() function using the self.PrecacheScriptSound(string soundScript) method.

Advanced Examples

Attaching objects to the viewmodel
Entities can be parented to attachment points on the viewmodel. This has some issues though. The attachment will only be visible on the client viewing the viewmodel, and the server console will be spammed with bone access errors.

Here is an example of attaching a small flame to a flamethrower nozzle.
local flamethrower_flame = { classname = "info_particle_system" angles = Vector(30, 0, 0) effect_name = "weapon_molotov_fp" render_in_front = "0" start_active = "1" targetname = "flamethrower_fire" origin = Vector(0, 0, 0) } flame <- g_ModeScript.CreateSingleSimpleEntityFromTable(flamethrower_flame) DoEntFire("!self", "SetParent", "!activator", 0, viewmodel, flame) DoEntFire("!self", "SetParentAttachment", "attach_nozzle", 0.01, flame, flame)
254 Comments
kouga 22 Jul @ 7:17pm 
ahhh well. the crude way works anyhow
Rectus  [author] 22 Jul @ 10:32am 
Not sure if that is possible. The scripting interface is pretty limited AFAIK.
kouga 21 Jul @ 11:47pm 
hey im back! ive got another feature from the fists mod that id like to tweak. see the fists mod works in conjunction with other mods that one way or another leave the secondary slot empty (by dropping or throwing). and to address that i used a rather crude way where i just give the player fists right right after those mods do their thing, instead of attaching it to the weapon script itself. this works, sure but it also leads to some annoyance because its not a universal feature— id really like it to be. and ive tried a different method before where it gave me fists on weapon_drop if my secondary slot was empty, but i ran into a bug where im genuinely unable to pickup any other melee because it just gives me the fists first.

how can i make it so that ***anytime*** (during spawn, revive, rescue, restart etc. INCLUDING bots) that a survivor has an empty secondary slot it will give fists, but without it causing them to be unable to pickup anything else? thanks
kouga 20 Jul @ 3:32pm 
ah i see! no problem ^^ thank you for the assistance! ill credit you if i get around to posting the mod ^^
Rectus  [author] 20 Jul @ 2:48pm 
Not really, i did all my animations from scratch if I remember right.
kouga 20 Jul @ 2:31pm 
thats alright! im very grateful for your massive help already ^^ do you also happen to know your way around .qc editing and such? id really like to use an alternate existing animation for the fists and reassign them to the custom melees models, but i wasnt confident if i was doing it right
Rectus  [author] 20 Jul @ 2:21pm 
I have no idea how those work, so can't help you there.
kouga 20 Jul @ 2:08pm 
ive figured it out! looks like it was a conflict with my weapon_melee script which let me use melees while incapped by changing the WeaponType to "pistol". its a shame though because I really want to include it in my modpack, so is there any way for both mods to work without causing the conflict? ive tried putting the weapon_melee script with the custom melee mod but that didnt seem to solve the issue
kouga 20 Jul @ 1:09pm 
interesting, well i do have the code isolated to the specific hands script, so im not sure whats goin on right now. im testing to find if its a mod conflict issue, will report back in a bit
Rectus  [author] 20 Jul @ 12:40pm 
If the code is in the individual weapon script, it will only be called on the weapons using that script. The whole point of how the weapons are set up with scripts is that it allows the differnt weapons handle the events differently.