Armes et outils en Lua

Écrire des armes entièrement en Lua (les SWEP de Garry's Mod), jeter et ramasser des armes au sol, créer des outils de toolgun (les STOOL) avec leur panneau de réglages, et contrôler le toolgun, le physgun et la touche E avec des hooks.

Armes Lua ou armes de l'éditeur ?L'éditeur d'armes crée des armes sans code (modèle, animations, réglages dans un weapon.json). Une arme Lua décrit son comportement en code, et peut reprendre le modèle et les animations d'une arme du jeu ou d'une arme de l'éditeur avec SWEP.Base.

Armes en Lua (SWEP)

Où les écrire

OùFichier
Gamemodeweapons/<classe>.lua
Addonlua/weapons/<classe>.lua
N'importe quel fichier partagéweapons.Register(SWEP, "classe")

Ces fichiers sont chargés côté serveur et côté client : le serveur fait les dégâts, le client donne le retour immédiat (son, traçantes, animation, chargeur affiché). Pendant le chargement, la table globale SWEP est prête (avec SWEP.Primary = {} et SWEP.Secondary = {}) ; la classe est le nom du fichier sans .lua, en minuscules. Nom de classe : lettres minuscules, chiffres et _, 48 caractères au plus, et pas le nom d'une arme du jeu (physgun, toolgun, crowbar, pistol, shotgun, rifle). 256 classes au plus.

Pour donner l'arme : ply:Give("classe") (serveur). Avec SWEP.Spawnable = true, elle apparaît aussi dans l'onglet Armes du menu de spawn.

Champs de SWEP

ChampDéfautRôle
PrintNamela classeNom affiché (sélecteur, HUD, menu).
Category"Armes Lua"Catégorie dans l'onglet Armes du menu de spawn.
Slot3Emplacement, compté à partir de 0 comme dans GMod : Slot = 0 → touche 1… 5 → touche 6.
SlotPos0Ordre dans l'emplacement (plus petit d'abord, puis par nom).
Spawnablefalsetrue : dans le menu de spawn ; sinon seulement par ply:Give.
AdminOnlyfalseDepuis le menu de spawn, réservée aux admins (et à l'hôte).
Base—Une autre classe Lua (héritage des champs et fonctions), ou une arme du jeu ("pistol", "shotgun", "rifle", "crowbar", "physgun", "toolgun"), ou l'id d'une arme de l'éditeur ("addon:<clé>/weapons/<fichier>.json"). Une arme du jeu donne le modèle, les animations (1re et 3e personne) et les valeurs par défaut de Primary (dégâts, cadence, dispersion, chargeur, son, rechargement).
Model—Un prop tenu en main à la place d'une arme ("builtin:…" ou "addon:<clé>/models/…glb") : pas d'animation, mouvements simples (recul, balancement, rechargement). WorldModel est accepté s'il commence par builtin: ou addon:.
ViewModel—Modèle de la vue 1re personne, id d'un modèle d'addon ou du jeu ("addon:<clé>/models/v_arme.glb", "builtin:…" ; les chemins models/…mdl de GMod sont ignorés). Il remplace la vue de la base (sauf si Base est une arme de l'éditeur et que Model est vide : la vue de l'éditeur reste) : voir Modèle de vue personnalisé. Un id inconnu affiche une erreur.
UseHands, ViewModelFlipfalseAvec ViewModel : ajoute la main du jeu au modèle (pour un modèle sans bras) ; retourne le modèle en miroir (arme à gauche).
ModelScale, ModelOffset, ModelAngle1, Vector(0, 0, 0)Échelle du prop (0,01 à 20), décalage dans la main (mètres, ±2 ; la poignée est à l'origine, le canon vers −Z), rotation (degrés).
ModelColorColor(255, 255, 255)Teinte du modèle : multiplie la couleur de ses matériaux, comme ent:SetColor sur un prop (alpha < 255 = transparent). Appliquée au modèle tenu en main en 3e personne, à la vue 1re personne d'un prop (Model), du physgun, du toolgun, et à l'arme jetée au sol. Pas sur la vue animée 1re personne d'une arme du jeu (bras compris) ni d'une arme de l'éditeur. Héritée par Base comme les autres champs.
Icon—Image du sélecteur et du HUD (image de materials/ ou "physicore/…png") ; sinon le modèle est rendu.
Instructions, Purpose—Aide affichée en bas du HUD quand l'arme est en main.
DrawCrosshairtruefalse cache le viseur du jeu.
ReloadTimebase, ou 1,5Durée de DefaultReload (secondes).
Primary.ClipSize−1Taille du chargeur (−1 = pas de chargeur).
Primary.DefaultClipClipSizeMunitions données avec l'arme : le chargeur est rempli, le reste va dans la réserve du type Ammo. Redonner l'arme ajoute DefaultClip à la réserve.
Primary.Ammo"none"Type de munitions (réserve partagée avec ply:GiveAmmo(n, type) et ply:GetAmmo(type)). "none" = réserve illimitée pour recharger.
Primary.AutomaticfalseTir continu en maintenant le clic.
Primary.Damage, NumShots (ou NumberofShots), Cone (ou Spread, en degrés), Delay, Sound, Recoil (degrés), Range (m), Force, TakeAmmo, DrySoundbaseUtilisés par la fonction PrimaryAttack par défaut.
Secondary.*Pareil pour le clic droit (ClipSize, DefaultClip, Ammo, Automatic).

Fonctions appelées par le jeu

FonctionCôtéQuand
SWEP:Initialize()les deuxCréation de l'objet arme du joueur (première utilisation).
SWEP:PrimaryAttack()les deuxClic gauche (répété si Primary.Automatic), en respectant SetNextPrimaryFire. Par défaut : CanPrimaryAttack → ShootBullet(Primary.Damage, Primary.NumShots, Primary.Cone) → ShootEffects → EmitSound(Primary.Sound) → TakePrimaryAmmo(1) → ViewPunch(Primary.Recoil) → SetNextPrimaryFire(CurTime() + Primary.Delay).
SWEP:SecondaryAttack()les deuxClic droit (rien par défaut).
SWEP:Reload()les deuxTouche R (par défaut self:DefaultReload()).
SWEP:Deploy() / SWEP:Holster()les deuxL'arme est prise en main / rangée. Côté client, Holster qui renvoie false garde l'arme en main (sauf changement imposé par le serveur ou arme retirée).
SWEP:Think()les deuxChaque image physique tant que l'arme est en main (serveur : pour chaque joueur ; client : le joueur local).
SWEP:Equip(owner)serveurL'arme vient d'être donnée.
SWEP:OnDrop()serveurL'arme est jetée (ply:DropWeapon).
SWEP:OnRemove()les deuxLe joueur n'a plus l'arme.
SWEP:DrawHUD()clientChaque image après HUDPaint, si l'arme est en main (draw.*, surface.*).
SWEP:GetViewModelPosition(pos, ang), SWEP:CalcViewModelView(…), SWEP:PreDrawViewModel(…), SWEP:PostDrawViewModel(…), SWEP:ViewModelDrawn(…)clientChaque image de la vue 1re personne du propriétaire : voir Modèle de vue personnalisé.
SWEP:DrawWorldModel(flags)clientChaque image où le modèle en main est visible (autres joueurs, 3e personne) : voir plus bas.

Côté client, PrimaryAttack, SecondaryAttack et Reload ne tournent que chez le propriétaire (prédiction) ; les autres joueurs voient le tir grâce au serveur.

Méthodes de l'arme (self)

MéthodeDescription
self:GetOwner() / self.OwnerLe joueur.
self:GetClass(), self:GetPrintName(), self:GetSlot()Classe, nom, emplacement.
self:IsValid(), self:IsActive()Le joueur a toujours l'arme ; elle est en main.
self:Clip1() / self:Clip2()Balles dans le chargeur (−1 sans chargeur). Compté par le serveur ; le client affiche sa prédiction.
self:SetClip1(n) / self:SetClip2(n)Change le chargeur.
self:GetMaxClip1() / GetMaxClip2()Primary.ClipSize / Secondary.ClipSize.
self:Ammo1() / self:Ammo2()Réserve du type Primary.Ammo / Secondary.Ammo (0 si "none").
self:TakePrimaryAmmo(n) / TakeSecondaryAmmo(n)Retire n balles du chargeur (de la réserve s'il n'y a pas de chargeur).
self:CanPrimaryAttack() / CanSecondaryAttack()false pendant un rechargement, ou chargeur vide (joue Primary.DrySound, par défaut le clic à vide, et bloque 0,25 s).
self:SetNextPrimaryFire(t) / SetNextSecondaryFire(t)Heure (CurTime()) avant laquelle le clic est ignoré (60 s plus tard au plus). Aussi GetNextPrimaryFire(), GetNextSecondaryFire().
self:ShootBullet(dégâts, nombre, dispersion, portée, poussée)Tir réel : nombre balles (1 à 32) dans un cône de dispersion degrés (0 à 45), portée en mètres (200 par défaut, 2000 au plus), poussée sur les props (dégâts × 1,5 par défaut). Le serveur trace depuis sa vue du joueur avec la même dispersion que le client, applique les dégâts (tête × 2, réglage « Dégâts entre joueurs », hooks PlayerShouldTakeDamage, ScalePlayerDamage, EntityTakeDamage pour les props) et montre traçantes et impacts aux autres.
self:FireBullets({Damage, Num, Spread, Distance, Force})Variante GMod ; Spread en degrés, ou un Vector(x, y, 0) comme dans GMod (converti). ShootBullet passe par elle. Avant le tir, le hook EntityFireBullets peut l'annuler ou changer la table.
self:ShootEffects()Éclair du canon, animation de tir (1re et 3e personne), recul de l'arme.
self:EmitSound(chemin, niveau, hauteur, volume)Son sur le joueur. Pendant un tir du joueur, le serveur l'envoie à tous sauf lui (il l'a déjà joué).
self:SendWeaponAnim(clip, durée)Animation de la vue 1re personne : "fire", "reload", "draw", "idle", "attack", "attack_hit", "inspect"…, ou les constantes ACT_VM_PRIMARYATTACK ("fire"), ACT_VM_SECONDARYATTACK, ACT_VM_RELOAD, ACT_VM_DRAW, ACT_VM_IDLE, ACT_VM_HOLSTER, ACT_VM_MISSCENTER ("attack"), ACT_VM_HITCENTER ("attack_hit"). "fire", "attack", "reload" et "draw" sont aussi joués en 3e personne.
self:DefaultReload(), self:IsReloading()Recharge depuis la réserve en SWEP.ReloadTime secondes (renvoie true si le rechargement commence) ; rechargement en cours ?
self:ViewPunch(degrés)Recul de la vue du propriétaire.
self:SetHoldType(type)Gardé dans self.HoldType (la pose vient de la base).
self:SetModelColor(couleur)Serveur : change la teinte de cette arme du joueur pendant la partie (remplace SWEP.ModelColor) ; envoyée à tous les joueurs, y compris ceux qui arrivent après. Reste valable si l'arme est redonnée ; Color(255, 255, 255) rend les couleurs d'origine. Sans effet côté client.
self:GetModelColor()Teinte actuelle : celle de SetModelColor, sinon SWEP.ModelColor (blanc par défaut).

Bibliothèque weapons

FonctionDescription
weapons.Register(SWEP, classe)Déclare une arme Lua (à faire des deux côtés).
weapons.GetStored(classe)La table SWEP, ou nil.
weapons.Get(classe)La table SWEP, ou les réglages d'une arme de l'éditeur.
weapons.IsLua(classe)true pour une arme Lua.
weapons.GetSWEPList()Toutes les tables SWEP.
weapons.GetList()Armes des addons, plus les armes Lua Spawnable (kind = "lua").

Exemple : pistolet laser

-- gamemodes/mon_mode/weapons/laser_pistol.lua (ou lua/weapons/laser_pistol.lua dans un addon)
SWEP.PrintName = "Pistolet laser"
SWEP.Category = "Armes de test"
SWEP.Slot = 1               -- touche 2
SWEP.Spawnable = true
SWEP.Base = "pistol"         -- modèle, animations et sons du pistolet du jeu
SWEP.Instructions = "Clic G : tirer · Clic D : rafale de 3 · R : recharger"

SWEP.Primary.ClipSize = 20
SWEP.Primary.DefaultClip = 60   -- 20 dans le chargeur, 40 en réserve
SWEP.Primary.Ammo = "laser"
SWEP.Primary.Automatic = false
SWEP.Primary.Damage = 18
SWEP.Primary.Delay = 0.2
SWEP.ReloadTime = 1.4

function SWEP:PrimaryAttack()
	if not self:CanPrimaryAttack() then return end
	self:ShootBullet(18, 1, 0.3, 300)          -- dégâts, balles, dispersion (°), portée (m)
	self:ShootEffects()
	self:EmitSound("physicore/weapons/pistol_fire.wav", 75, 140)
	self:TakePrimaryAmmo(1)
	self:ViewPunch(0.6)
	self:SetNextPrimaryFire(CurTime() + 0.2)
end

function SWEP:SecondaryAttack()
	if self:Clip1() < 3 then return end
	self:ShootBullet(14, 3, 2.5)
	self:ShootEffects()
	self:EmitSound("physicore/weapons/pistol_fire.wav", 75, 90)
	self:TakePrimaryAmmo(3)
	self:SetNextPrimaryFire(CurTime() + 0.6)
	self:SetNextSecondaryFire(CurTime() + 0.6)
end

if CLIENT then
	function SWEP:DrawHUD()
		draw.SimpleText("Laser : " .. self:Clip1(), "Default", ScrW() / 2, ScrH() / 2 + 40, Color(255, 80, 80), 1, 1)
	end
end

Pour le donner : ply:Give("laser_pistol") ; des munitions en plus : ply:GiveAmmo(40, "laser").

Exemple : un prop comme arme

SWEP.PrintName = "Planche à claques"
SWEP.Slot = 2
SWEP.Spawnable = true
SWEP.Model = "builtin:plank"
SWEP.ModelScale = 0.35
SWEP.ModelAngle = Vector(0, 90, 0)   -- la planche dans l'axe de la vue
SWEP.ModelOffset = Vector(0, 0, -0.2)
SWEP.ModelColor = Color(255, 120, 120)  -- planche teintée de rouge
SWEP.Primary.ClipSize = -1
SWEP.Primary.Ammo = "none"
SWEP.DrawCrosshair = false

function SWEP:PrimaryAttack()
	self:SendWeaponAnim(ACT_VM_MISSCENTER)          -- mouvement de frappe
	self:ShootBullet(35, 1, 0, 2, 400)               -- portée 2 m, grosse poussée
	self:EmitSound("physicore/weapons/crowbar_swing.wav")
	self:SetNextPrimaryFire(CurTime() + 0.6)
end

Exemple : héritage

-- weapons/laser_rifle.lua : reprend tout du pistolet laser, en automatique.
SWEP.Base = "laser_pistol"
SWEP.PrintName = "Fusil laser"
SWEP.Slot = 3
SWEP.Primary.Automatic = true
SWEP.Primary.ClipSize = 40

Modèle de vue personnalisé

SWEP.ViewModel donne à l'arme sa propre vue 1re personne : un modèle d'un addon ("addon:<clé>/models/v_arme.glb", importé comme un prop) ou du jeu ("builtin:…"). Il est placé dans le repère de la caméra : son origine est l'œil du joueur, l'avant vers −Z, comme les modèles de vue des armes du jeu (modélise-le bras compris, tel qu'il doit apparaître à l'écran). Il suit le balancement et la marche du jeu. Le modèle en main pour les autres joueurs reste Model / WorldModel, ou celui de la base, et la pose en 3e personne suit la base.

Fonctions client, appelées à chaque image de la vue 1re personne du propriétaire (pour toute arme Lua dont la vue vient d'une arme du jeu, d'un prop ou de ViewModel ; pas quand la base est une arme de l'éditeur sans Model) :

FonctionRôle
SWEP:GetViewModelPosition(pos, ang)Reçoit la position et les angles dans le monde du modèle de vue (après balancement et marche) ; renvoyer pos, ang le déplace. Ne rien renvoyer le laisse en place
SWEP:CalcViewModelView(vm, ancienPos, ancienAng, pos, ang)Pareil, appelée après la précédente (les deux paires valent la même position) ; renvoyer pos, ang
Hook CalcViewModelView(wep, vm, ancienPos, ancienAng, pos, ang)Pareil pour toutes les armes Lua, en dernier
SWEP:PreDrawViewModel(vm, wep, ply), hook PreDrawViewModel(vm, ply, wep)Renvoyer true cache le modèle de vue pour cette image
SWEP:PostDrawViewModel(vm, wep, ply), SWEP:ViewModelDrawn(vm), hook PostDrawViewModel(vm, ply, wep)Après, quand le modèle est montré (pour dessiner par-dessus avec le dessin 3D, par exemple)
SWEP:DrawWorldModel(flags)Chaque image où le modèle en main d'un joueur est visible (chez chaque joueur qui le voit). Si la classe la définit, le modèle n'est montré que si elle appelle self:DrawModel() ; sans elle, il est toujours montré

vm vaut toujours nil : il n'y a pas d'entité de modèle de vue. SWEP.ViewModelFOV n'est pas géré.

-- weapons/fusil_scifi.lua : modèle de vue d'un addon, abaissé quand on court, caché en visant.
SWEP.PrintName = "Fusil SF"
SWEP.Base = "rifle"                -- tir, chargeur et pose en 3e personne du fusil du jeu
SWEP.Spawnable = true
SWEP.ViewModel = "addon:mon_pack/models/v_fusil_sf.glb"   -- clips draw, idle, fire, reload
SWEP.WorldModel = "addon:mon_pack/models/w_fusil_sf.glb"

if CLIENT then
	function SWEP:GetViewModelPosition(pos, ang)
		local ply = self:GetOwner()
		if ply:KeyDown(IN_SPEED) and ply:GetVelocity():Length() > 4 then
			return pos - ang:Up() * 0.06, ang + Vector(-12, 8, 0)
		end
	end

	function SWEP:PreDrawViewModel(vm, wep, ply)
		return ply:KeyDown(IN_ATTACK2)   -- lunette : on cache le modèle
	end

	function SWEP:DrawWorldModel(flags)
		if not self:GetOwner():GetNWBool("cache_arme") then self:DrawModel() end
	end
end

Hook EntityFireBullets

EntityFireBullets(tireur, balle) est appelé à chaque FireBullets (donc à chaque ShootBullet), côté serveur et côté client (chez le tireur, pour sa prédiction). balle est la table du tir : Damage, Num, Spread, Distance, Force (les champs absents prennent leur valeur par défaut). Renvoyer false annule le tir ; on peut modifier la table (renvoyer true, comme dans GMod) pour changer le tir. Seules les armes Lua passent par ce hook.

-- shared.lua : dégâts doublés pour l'équipe 2, pas de tir dans la zone de départ.
hook.Add("EntityFireBullets", "regles_tir", function(tireur, balle)
	if tireur:GetPos():Distance(Vector(0, 0, 0)) < 10 then return false end
	if tireur:Team() == 2 then
		balle.Damage = (balle.Damage or 10) * 2
		return true
	end
end)
LimitesLe visuel vient de la base (arme du jeu, arme de l'éditeur), d'un prop ou de SWEP.ViewModel ; la pose en 3e personne suit la base, et ViewModelFOV n'est pas géré. Une classe déclarée seulement côté serveur fait des dégâts, mais sans effets immédiats chez le tireur ni icône dans son sélecteur : déclare les armes dans un fichier partagé. Le serveur accepte au plus 30 actions d'arme par seconde et par joueur.

Armes au sol et sélection

MéthodeCôtéDescription
ply:Give(id)serveurMarche aussi pour une classe Lua (avec Primary.DefaultClip).
ply:SelectWeapon(id)serveur / clientServeur : le joueur prend l'arme (sans le hook PlayerSwitchWeapon). Client : seulement pour LocalPlayer(), en passant par le hook.
ply:DropWeapon(id)serveurL'arme (par défaut celle en main) tombe devant le joueur avec ses munitions (chargeur, et réserve propre pour les armes du jeu). Renvoie true si elle a été jetée.
ply:GetWeapon(classe)partagéL'objet arme Lua si le joueur l'a, sinon nil.
ply:GetActiveWeaponTable()partagéL'objet arme Lua en main, sinon nil (ply:GetActiveWeapon() renvoie toujours l'id en texte).
ply:GetClip(id)partagéChargeur d'une arme (du jeu, de l'éditeur ou Lua).

Une arme au sol se ramasse en marchant dessus ou en la visant avec E. Si le joueur a déjà l'arme, il ne prend que les munitions (l'arme reste au sol s'il n'y en a pas). Celui qui l'a jetée ne la ramasse pas en marchant pendant 1,5 s. 64 armes au sol au plus (les plus anciennes disparaissent). Elle ne bloque pas les joueurs.

-- init.lua : on lâche son arme à la mort ; les armes lourdes sont interdites aux civils.
local TEAM_CIVIL = 1 -- numéro d'équipe déclaré avec team.SetUp

hook.Add("PlayerDeath", "lacher_arme", function(victime)
	local id = victime:GetActiveWeapon()
	if id ~= "" and id ~= "physgun" and id ~= "toolgun" then victime:DropWeapon(id) end
end)

hook.Add("PlayerCanPickupWeapon", "civils", function(ply, id)
	if ply:Team() == TEAM_CIVIL and (id == "rifle" or id == "shotgun") then return false end
end)

concommand.Add("jeter", function(ply) ply:DropWeapon() end)
-- shared.lua : pas de changement d'arme pendant qu'on vise avec le clic droit.
hook.Add("PlayerSwitchWeapon", "pas_en_visant", function(ply, ancienne, nouvelle)
	if ply:IsKeyDown(MOUSE_RIGHT) then return true end
end)

Outils de toolgun (TOOL)

Où les écrire

OùFichier
Gamemodestools/<nom>.lua
Addonlua/weapons/gmod_tool/stools/<nom>.lua
N'importe quel fichier partagétool.Register(TOOL, "nom")

Chargé côté serveur et côté client. La table globale TOOL est prête (TOOL.ClientConVar = {}) ; le nom de l'outil est le nom du fichier en minuscules (minuscules, chiffres et _, pas le nom d'un outil du jeu). L'outil apparaît dans la liste des outils du menu de spawn.

Champs de TOOL

ChampRôle
TOOL.NameNom dans la liste des outils et sur l'écran du toolgun.
TOOL.CategoryCatégorie de la liste ("Lua" par défaut) ; peut être une catégorie du jeu ("Construction", "Édition"…).
TOOL.Desc (ou Help, ou Information en texte)Aide affichée en bas du HUD et dans le menu.
TOOL.ClientConVar = { cle = défaut }Réglages du joueur. Un nombre (ou le texte d'un nombre, comme "255" dans GMod) donne un champ numérique, un booléen une case à cocher, une Color un sélecteur de couleur, un autre texte un champ texte. Gardés sur l'ordinateur du joueur. 64 au plus.
function TOOL.BuildCPanel(panel)Client, fonction statique (avec un point) : construit le panneau de réglages du menu de spawn à la place des champs automatiques.

Fonctions appelées par le jeu

FonctionQuand
TOOL:LeftClick(trace)Clic gauche. Renvoyer true montre le tir du toolgun (traçante).
TOOL:RightClick(trace)Clic droit.
TOOL:Reload(trace)Touche R.
TOOL:Think()Chaque image physique tant que l'outil est en main.
TOOL:Deploy() / TOOL:Holster()Le toolgun est sorti sur cet outil / l'outil est quitté.

Ces fonctions tournent des deux côtés : côté client pour le retour immédiat, mais les modifications du monde se font côté serveur (if CLIENT then return true end). trace contient Hit, HitPos, HitNormal, HitWorld, HitNonWorld, Entity (prop touché ou nil), StartPos (les yeux), Normal (direction) et Fraction.

Méthodes de l'outil (self)

MéthodeDescription
self:GetOwner() / self.OwnerLe joueur.
self:GetMode()Nom de l'outil.
self:GetClientInfo(cle)Réglage du joueur en texte (booléen "1" / "0", couleur "r g b a"). Côté serveur : les valeurs envoyées avec le dernier clic (ou quand le joueur change un réglage), vérifiées.
self:GetClientNumber(cle, défaut), self:GetClientBool(cle)Réglage en nombre, en booléen.
self:GetStage() / self:SetStage(n)Étape libre, pour les outils en plusieurs clics.

Panneau de réglages

Méthode de panelDescription
Help(texte), ControlHelp(texte), Header(texte)Textes.
NumSlider(texte, convar, min, max, décimales)Champ numérique lié à un réglage. convar = "<outil>_<cle>" (style GMod) ou "<cle>".
CheckBox(texte, convar), TextEntry(texte, convar)Case à cocher, champ texte.
ComboBox(texte, convar, options)Liste : { "a", "b" } ou { ["Libellé"] = valeur }.
ColorPicker(texte, convar)Couleur (réglage Color), ou ColorPicker(texte, "x_r", "x_g", "x_b", "x_a") pour 3 ou 4 réglages de 0 à 255.
Button(texte, fonction)Bouton qui appelle la fonction (client).
AddItem(panneau)Ajoute un panneau créé avec vgui.Create.
AddControl(type, params)Style GMod : "Header" / "Label" (Description / Text), "Slider" (Label, Command, Min, Max, Type = "Integer"), "CheckBox", "TextBox", "Color" (Red, Green, Blue, Alpha).
ClearControls()Vide le panneau.

Un réglage absent de ClientConVar affiche une erreur Lua. 200 contrôles au plus. Bibliothèque : tool.Register(TOOL, nom), tool.Get(nom) (la table TOOL), tool.GetList() (noms triés).

Exemple : peinture rouge

-- gamemodes/mon_mode/stools/rouge.lua (ou lua/weapons/gmod_tool/stools/rouge.lua)
TOOL.Name = "Peinture rouge"
TOOL.Category = "Édition"
TOOL.Desc = "Clic G : peindre en rouge · Clic D : couleur d'origine · R : intensité actuelle"
TOOL.ClientConVar = { intensite = "255", transparent = false }

function TOOL:LeftClick(trace)
	local ent = trace.Entity
	if not IsValid(ent) then return false end
	if CLIENT then return true end                 -- le client montre juste le tir
	local r = math.Clamp(self:GetClientNumber("intensite", 255), 0, 255)
	local a = self:GetClientBool("transparent") and 120 or 255
	ent:SetColor(Color(r, 0, 0, a))
	return true
end

function TOOL:RightClick(trace)
	if not IsValid(trace.Entity) then return false end
	if SERVER then trace.Entity:SetColor(Color(255, 255, 255)) end
	return true
end

function TOOL:Reload(trace)
	if CLIENT then return false end
	self:GetOwner():ChatPrint("Intensité actuelle : " .. self:GetClientInfo("intensite"))
	return false
end

function TOOL.BuildCPanel(panel)
	panel:Help("Peint les props en rouge.")
	panel:NumSlider("Intensité du rouge", "rouge_intensite", 0, 255, 0)
	panel:CheckBox("Semi-transparent", "rouge_transparent")
end

Toolgun, physgun et touche E

HookCôtéDescription
CanTool(ply, trace, outil)serveur et clientAvant chaque action du toolgun (outils du jeu et outils Lua ; clic, clic droit, R). false refuse. outil : id de l'outil ("remover", "color", "weld"…, ou le nom d'un outil Lua). Pour une contrainte, appelé pour chacun des deux props. Côté client, l'action n'est même pas envoyée.
PhysgunPickup(ply, ent)serveurfalse : le physgun ne peut pas l'attraper.
PhysgunDrop(ply, ent)serveurLe joueur lâche un prop du physgun.
OnPhysgunFreeze(ply, ent)serveurClic droit en tenant un prop : false le lâche sans le figer.
CanPlayerUnfreeze(ply, ent)serveurR au physgun sur un prop figé : false le garde figé.
PlayerUse(ply, ent)serveurE sur n'importe quel prop à portée (4 m du bord du prop, plus pour un véhicule) : false bloque l'usage (monter dans un véhicule, ENT:Use, bouton d'un gadget, ramasser une arme au sol). 8 usages par seconde au plus.
-- Protection des props : seul le propriétaire (ou un admin) peut les modifier.
hook.Add("CanTool", "proprio", function(ply, trace, outil)
	local ent = trace and trace.Entity
	if IsValid(ent) and ent:GetOwner() ~= ply and not ply:IsAdmin() then
		if SERVER then ply:ChatPrint("Ce prop n'est pas à toi.") end
		return false
	end
end)

-- Seule la police peut monter dans les voitures de police.
local TEAM_POLICE = 2
hook.Add("PlayerUse", "voitures_police", function(ply, ent)
	if ent:IsVehicle() and ent:GetNWBool("police") and ply:Team() ~= TEAM_POLICE then return false end
end)

Voir aussi la liste complète des hooks.