-
Notifications
You must be signed in to change notification settings - Fork 58
Stat Handling
Weapon attributes in ARC9 are incredibly dynamic and easy to manipulate. TODO more introduction text
Use SWEP:GetProcessedValue(base, cached)
to access the current value of any stat.
If cached
is true, ARC9 will try to store the resulting value for a short time (<1s) and use it if possible. Use this if you are accessing the same stat frequently and do not need the most up to date value.
local wep = Entity(1):Give("arc9_sexydeagle")
-- Tell us the clip size of the weapon. This checks for all modifiers on the weapon (from attachments etc.).
print("Our weapon has a clip size of " .. wep:GetProcessedValue("ClipSize"))
-- This returns the unmodified value. Do not ever modify these variables directly!
print("The original clip size is " .. wep.ClipSize)
This function is the bread and butter of ARC9 - you should NEVER have to modify the original variable.
A stat is any variable that is or potentially could be on the weapon. When ARC9 accesses a stat, it applies all modifiers from different sources to the stat in the GetProcessedValue
function, so that the return value reflects the changes.
To make changes to a stat in an attachment, define a variable with the following format:
Base + Modifier + Condition (optional, up to 1)
Here are some examples.
-- Multiply close range damage by 2.
-- Base: DamageMax
-- Modifier: Mult
ATT.DamageMaxMult = 2
-- Add up to 4 pellets when firing from the hip.
-- Base: Num
-- Modifier: Add
-- Condition: HipFire
ATT.NumAddHipFire = 4
-- Deal 9001 melee damage when the gun is empty.
-- Base: MeleeDamage
-- Modifier: Override
-- Condition: Empty
ATT.MeleeDamageOverrideEmpty = 9001
-- Reduce heat per shot by up to 50% based on the player's armor.
-- Base: HeatPerShot
-- Modifier: Hook
ATT.HeatPerShotHook = function(wep, data)
if wep:GetOwner():IsPlayer() and wep:GetOwner():Armor() > 0 then
return data * (1 - math.min(0.5, wep:GetOwner():Armor() / wep:GetOwner():GetMaxArmor()))
end
end
The original value of a stat is defined in the SWEP
table. The variable name here is known as the "Base" of the stat. This name is what you would put into GetProcessedValue
.
-- lua/weapons/arc9_sexydeagle.lua
SWEP.PrintName = "Sexy Deagle"
SWEP.DamageMax = 20
SWEP.DamageMin = 15
-- other boring SWEP variables etc.
-- wep:GetProcessedValue("DamageMax") would give 20 without any additional modifiers.
You can define new Bases in attachments and check for their existence with GetProcessedValue
.
-- In attachment foo:
ATT.FunnyNumber = 69
-- In attachment bar:
ATT.FunnyNumber = 420
-- Check if the weapon has the "FunnyNumber" stat.
-- This would return 69 if foo is attached, 420 if bar is attached, and nil if neither are.
-- If both are attached, the result is not guaranteed to be deterministic. Use a _Priority to resolve conflicts.
print(wep:GetProcessedValue("FunnyNumber"))
Modifiers are how you make changes to an existing Base stat. There are 4 types of modifiers: Override, Mult, Add, and Hook, processed in that order.
Add and Mult modifiers only apply to numeric stats, while Override and Hook modifiers work on all stats.
The Override modifier sets the stat to the provided value. This is commonly used on stats with non-numeric values since it is the only way to change them.
Override modifiers can have a priority. This is set by adding a separate variable with _Priority
appended to it. Priority defaults to 1.
No matter how low the priority is, a override modifier will still take effect if no other override exists.
-- Overrides melee damage to burning.
ATT.BashDamageTypeOverride = DMG_BURN
-- This override has a higher priority than normal.
ATT.BashDamageTypeOverride_Priority = 2
The Mult modifier multiplies the stat by the provided value.
-- Multiplies close range damage by 1.5.
ATT.DamageMaxMult = 1.5
The Add modifier adds the provided value to the stat. Note that this happens after Mult modifiers.
-- Adds 1 to clip size.
ATT.ClipSizeAdd = 1
-- Subtracts 10 from long range damage.
ATT.DamageMinAdd = -10
The Hook modifier is special, as it calls the provided value as a function, and takes the return value (if any) for the stat. This allows you to do all sorts of exciting and weird things.
Hooks also use the same priority rules as Override hooks.
-- The weapon costs more bullets to fire the more health the player has.
ATT.AmmoPerShotHook = function(wep, stat)
if wep:GetOwner():IsPlayer() then
return math.Round(Lerp(wep:GetOwner():Health() / wep:GetOwner():MaxHealth(), 1, 10))
end
end
-- Put this in all your guns so I get double damage when using them.
ATT.DamageMaxHook = function(wep, stat)
if wep:GetOwner():IsPlayer() and wep:GetOwner():SteamID64() == "76561198027025876" then
return stat * 2
end
end
-- Very important!
ATT.DamageMaxHook_Priority = 9999
Condition is an optional last piece of the puzzle for your stat. It determines when, and how strongly, the modifier applies. Each condition behaves differently, but most are either binary or scalar.
Binary conditions either apply the stat entirely or does not apply it at all.
For example, the MidAir
condition applies the modifier when the player is in the air, and does not apply when on the ground.
-- Multiplies spread by 2 when in the air. Does nothing when on the ground.
ATT.SpreadMultMidAir = 2
Scalar conditions interpolate the modifier based on the state of the condition. For non-numeric stats, the modifier is applied as long as the interpolated value is greater than zero.
An example is the Sights
condition. This condition scales based on how fully aimed the weapon is. If the player is partially zoomed in, the multiplier applies proportionally. For non-numeric stats, the condition applies as long as the player is partially aimed in.
The counterpart to Sights
is Sighted
, which is binary and only applies when the weapon is fully aimed, taking priority over Sights
if a stat has both conditions in the same attachment.
-- Multiplies spread by up to 0.1 when aiming.
ATT.SpreadMultSights = 0.1
-- When the player is 50% zoomed in, the multiplier would be `1 + 1 * (0.1 - 1) = 0.1`.
-- When the player is 50% zoomed in, the multiplier would be `1 + 0.5 * (0.1 - 1) = 0.55`.
-- When the player is 0% zoomed in, the multiplier would be `1 + 0 * (0.1 - 1) = 1`.
-- Multiplies damage by up to 1.5 when aiming.
ATT.DamageMaxMultSights = 1.5
-- The weapon becomes capable of overheating as soon as the gun starts aiming.
ATT.OverheatOverrideSights = true
-- Sighted is a separate binary condition that only applies when fully sighted.
-- It takes priority over Sights, so when fully aimed the weapon will do 3x damage instead of 1.5x.
ATT.DamageMaxMultSighted = 3
See https://github.com/HaodongMo/ARC-9/wiki/List-of-Conditions.
Sometimes you want to modify a stat outside the context of being an attachment, like adding status effects on the player, or external levelling / buff systems.
To accomplish this, use a normal GMod hook the same way you would a Hook modifier, prefixing ARC9_
to the hook identifier.
-- Players with "Cowboy" in their name can draw their weapon very quickly!
hook.Add("ARC9_DeployTimeHook", "yeehaw", function(wep, data)
local ply = wep:GetOwner()
if IsValid(ply) and ply:IsPlayer() and string.find(string.lower(ply:GetName()), "cowboy") then
return data * 0.1
end
end)
Each stat is handled more automatically than ever. Each stat has a "Base", such as Speed or ReloadTime or Sway. To create a new modifier to it, we add "Mult", e.g. SpeedMult. To make it conditional, we can then add a condition onto it, e.g. SpeedMultSights. This will make adding new conditions easier than ever, as it only needs to be modified in one place - the condition handler.
To be absolutely clear, this means you can make a nearly unlimited number of stat modifiers.
Each condition is handled individually according to the specific condition. For instance, the MidAir condition is binary - so modifiers with this condition are applied only if the player is in mind-air. However, the Sighted condition will interpolate between 0 and 1 multiplied by the total multiplier. So if the player is 50% aimed down their sights, and they have an attachment with SpreadMultSighted of 2, their spread will be multiplied by 1.5x. Mults are multiplied together before this happens - if the player has two attachments, one with SpreadMultSighted of 2 and one with 3, ARC9 will only save "6" and thus if the player is halfway sighted, their spread is multiplied by 3.
Modifier classes are Override, Mult, Add, and Hook, processed in that order, as in ArcCW.
Override and Hook modifiers also support _Priority values being assigned to the same attachment or the base weapon for greater control over the order in which they are run. For instance. SpreadOverrideSighted_Priority = 2 on an attachment will cause that attachment's SpreadOverrideSighted to take precedence over all SpreadOverrideSighted values without a priority set or with priority less than 2. Unset priority counts as 1.
Examples:
SpeedMult AimDownSightTimeHook AimDownSightOverrideCrouch
ArcCW: Mult_SpeedMult ARC9: SpeedMult
ArcCW: Override_Firemodes ARC9: FiremodesOverride
Hooks make a return outside of stat modifications. Hooks (Like "HookT_MinimapPing") are now separated into different types based on how they accept return data. Hooks that do not have a type ("Hook_Whatever") do not accept any return data and are only present for signalling. Both Hooks and Overrides accept _Priority, just like in ArcCW. Higher priority will be run with precedence. For instance, Hook_RecoilSeed_Priority = 3 means that attachment's Hook_RecoilSeed will be run before any other hook that belongs to an attachment with Hook_RecoilSeed_Priority = 2.
The base weapon is also considered.
HookT: Table hooks. Return a {table} of values. Every hook's returned values are gathered into one big table for later use. Every returned value is used. HookA: Accumulator hooks. Return an Angle, Vector, or Number - the resulting value is made up of all of these values added together. HookP: Pipeline hooks. Accept previously returned data, return new data. Each function modifies the data given. Return nil to make no change. Used for things like animation translate. HookC: Check hooks. Return false to prevent something from happening.
Hooks run in an order that can be compared as such:
- If A's [Hook_*]_Priority value is higher than B (Unspecified is treated as 1) then run it first.
- If A belongs to a top level slot with lower value than B, run it first.
- If A and B belong to the same top level slot, the one with the lowest depth will be run first.
- If A and B have the same depth, the one that belongs to the lowest level sub-slot will be run first.
This is, essentially, a breadth-first tree unwrap which treats each top level slot as its own tree and processes them in numerical order.