Skip to content

Weapon

A Weapon is an Equipment that comes with built-in Abilities and fires Projectiles.

Properties

Property Name Return Type Description Tags
animationStance string When the Weapon is equipped this animation stance is applied to the Player. Read-Only
attackCooldownDuration number Interval between separate burst sequences. The value is set by the Shoot ability's Cooldown duration. Read-Only
multiShotCount integer Number of Projectiles/Hitscans that will fire simultaneously inside the spread area each time the Weapon attacks. Does not affect the amount of ammo consumed per attack. Read-Only
burstCount integer Number of automatic activations of the Weapon that generally occur in quick succession. Read-Only
shotsPerSecond number Used in conjunction with burstCount to determine the interval between automatic weapon activations. Read-Only
shouldBurstStopOnRelease boolean If true, a burst sequence can be interrupted by the Player by releasing the action button. If false, the burst continues firing automatically until it completes or the Weapon runs out of ammo. Read-Only
isHitscan boolean If false, the Weapon will produce simulated Projectiles. If true, it will instead use instantaneous line traces to simulate shots. Read-Only
range number Max travel distance of the Projectile (isHitscan = False) or range of the line trace (isHitscan = True). Read-Only
damage number Damage applied to a Player when the weapon attack hits a player target. If set to zero, no damage is applied. Read-Only
projectileTemplateId string Asset reference for the visual body of the Projectile, for non-hitscan Weapons. Read-Only
muzzleFlashTemplateId string Asset reference for a Vfx to be attached to the muzzle point each time the Weapon attacks. Read-Only
attackSoundTemplateId string Asset reference for a sound effect to be played each time the Weapon attacks. Read-Only
trailTemplateId string Asset reference for a trail Vfx to follow the trajectory of the shot. Read-Only
beamTemplateId string Asset reference for a beam Vfx to be placed along the trajectory of the shot. Useful for hitscan Weapons or very fast Projectiles. Read-Only
impactSurfaceTemplateId string Asset reference of a Vfx to be attached to the surface of any CoreObjects hit by the attack. Read-Only
impactProjectileTemplateId string Asset reference of a Vfx to be spawned at the interaction point. It will be aligned with the trajectory. If the impacted object is a CoreObject, then the Vfx will attach to it as a child. Read-Only
impactPlayerTemplateId string Asset reference of a Vfx to be spawned at the interaction point, if the impacted object is a player. Read-Only
projectileSpeed number Travel speed (cm/s) of Projectiles spawned by this weapon. Read-Only
projectileLifeSpan number Duration after which Projectiles are destroyed. Read-Only
projectileGravity number Gravity scale applied to spawned Projectiles. Read-Only
projectileLength number Length of the Projectile's capsule collision. Read-Only
projectileRadius number Radius of the Projectile's capsule collision Read-Only
projectileDrag number Drag on the Projectile. Read-Only
projectileBounceCount integer Number of times the Projectile will bounce before it's destroyed. Each bounce generates an interaction event. Read-Only
projectilePierceCount integer Number of objects that will be pierced by the Projectile before it's destroyed. Each pierce generates an interaction event. Read-Only
maxAmmo integer How much ammo the Weapon starts with and its max capacity. If set to -1 then the Weapon has infinite capacity and doesn't need to reload. Read-Only
currentAmmo integer Current amount of ammo stored in this Weapon. Read-Write
ammoType string A unique identifier for the ammunition type. Read-Only
isAmmoFinite boolean Determines where the ammo comes from. If true, then ammo will be drawn from the Player's Resource inventory and reload will not be possible until the Player acquires more ammo somehow. If false, then the Weapon simply reloads to full and inventory Resources are ignored. Read-Only
outOfAmmoSoundId string Asset reference for a sound effect to be played when the Weapon tries to activate, but is out of ammo. Read-Only
reloadSoundId string Asset reference for a sound effect to be played when the Weapon reloads ammo. Read-Only
spreadMin number Smallest size in degrees for the Weapon's cone of probability space to fire Projectiles in. Read-Only
spreadMax number Largest size in degrees for the Weapon's cone of probability space to fire Projectiles in. Read-Only
spreadAperture number The surface size from which shots spawn. An aperture of zero means shots originate from a single point. Read-Only
spreadDecreaseSpeed number Speed at which the spread contracts back from its current value to the minimum cone size. Read-Only
spreadIncreasePerShot number Amount the spread increases each time the Weapon attacks. Read-Only
spreadPenaltyPerShot number Cumulative penalty to the spread size for successive attacks. Penalty cools off based on spreadDecreaseSpeed. Read-Only

Functions

Function Name Return Type Description Tags
HasAmmo() boolean Informs whether the Weapon is able to attack or not. None
Attack(target) None Triggers the main ability of the Weapon. Optional target parameter can be a Vector3 world position, a Player, or a CoreObject. None

Events

Event Name Return Type Description Tags
targetImpactedEvent Event<Weapon weapon, ImpactData impactData> Fired when a Weapon interacts with something. For example a shot hits a wall. The ImpactData parameter contains information such as which object was hit, who owns the Weapon, which ability was involved in the interaction, etc. Server-Only
projectileSpawnedEvent Event<Weapon weapon, Projectile projectile> Fired when a Weapon spawns a projectile. None

Examples

Example using:

projectileSpawnedEvent

Although it is ineffective to modify a projectile that comes through the projectileSpawnedEvent, it's still a useful event for various gameplay mechanics. In this example, a weapon script adds recoil impulse in the opposite direction of shots.

local WEAPON = script:FindAncestorByType('Weapon')
local KNOCKBACK_SPEED = 1000

-- Adds impulse to the owner once the attack ability is executed
function OnProjectileSpawned(weapon, projectile)
    local player = weapon.owner

    local projectileDirection = projectile:GetWorldTransform():GetForwardVector()
    local knockbackVector = projectileDirection * player.mass * -KNOCKBACK_SPEED

    -- Push the player away from the spawned projectile
    player:AddImpulse(knockbackVector)
end

WEAPON.projectileSpawnedEvent:Connect(OnProjectileSpawned)

See also: Equipment.owner | Player.AddImpulse | CoreObject.FindAncestorByType | Projectile.GetWorldTransform | Transform.GetForwardVector | Vector3 * Number


Example using:

targetImpactedEvent

In this example, a weapon has a healing mechanic, where the player gains 2 hit points each time they shoot an enemy player.

local WEAPON = script:FindAncestorByType('Weapon')

function OnTargetImpactedEvent(weapon, impactData)
    if impactData.targetObject and impactData.targetObject:IsA("Player") then
        weapon.owner.hitPoints = weapon.owner.hitPoints + 2
    end
end

WEAPON.targetImpactedEvent:Connect(OnTargetImpactedEvent)

See also: Equipment.owner | CoreObject.FindAncestorByType | ImpactData.targetObject | other.IsA | Player.hitPoints | Event.Connect


Example using:

Attack

Generally, weapons are thought to be equipped on players. However, a weapon can be used on an NPC such as a vehicle or tower by calling the Attack() function. In this example, a weapon simply fires each second. Shots will go out straight in the direction the weapon is pointing.

local WEAPON = script:FindAncestorByType('Weapon')

function Tick()
    WEAPON:Attack()
    Task.Wait(1)
end

See also: CoreObject.FindAncestorByType | CoreLua.Tick | Task.Wait


Example using:

HasAmmo

In this example, a custom sound is played when someone picks up a weapon that has no ammo in it. For this hypothetical game, weapons can be found without any ammo and it's an important mechanic. It should be displayed in the user interface. However, players hear sound effects much faster than they can read UI.

local WEAPON = script:FindAncestorByType('Weapon')
local EMPTY_PICKUP_SOUND = script:GetCustomProperty("EmptyPickupSound")

function OnEquipped(weapon, player)
    if (not weapon:HasAmmo()) then
        World.SpawnAsset(EMPTY_PICKUP_SOUND, {position = weapon:GetWorldPosition()})
    end
end

WEAPON.equippedEvent:Connect(OnEquipped)

See also: Equipment.equippedEvent | CoreObject.FindAncestorByType | World.SpawnAsset | Event.Connect


Example using:

ammoType

isAmmoFinite

currentAmmo

In this simple auto-reload script, the weapon's current ammo is monitored. If it goes to zero and the player has ammo of the correct type, then the reload ability is activated. This script only works in a client-context and expects the Reload ability to be assigned as a custom property.

local WEAPON = script:FindAncestorByType('Weapon')
local RELOAD_ABILITY = script:GetCustomProperty("Reload"):WaitForObject()

local LOCAL_PLAYER = Game.GetLocalPlayer()

function Tick(deltaTime)
    if WEAPON.owner ~= LOCAL_PLAYER then return end

    if WEAPON.currentAmmo == 0 and
    (not WEAPON.isAmmoFinite or LOCAL_PLAYER:GetResource(WEAPON.ammoType) > 0) then

        RELOAD_ABILITY:Activate()

        Task.Wait(
            RELOAD_ABILITY.castPhaseSettings.duration +
            RELOAD_ABILITY.executePhaseSettings.duration +
            RELOAD_ABILITY.recoveryPhaseSettings.duration)
    end
end

See also: Weapon.currentAmmo | Equipment.owner | Ability.Activate | AbilityPhaseSettings.duration | CoreObject.FindAncestorByType | CoreObjectReference.WaitForObject | Game.GetLocalPlayer | Player.GetResource | Task.Wait | CoreLua.Tick


Example using:

animationStance

A weapon's animationStance is assigned to the player automatically when the item is equipped. In this example, we add an additional stance to the weapon in the form of a defensive posture that players can trigger by holding down the secondary ability button (mouse right-click). The script alternates between the shield block stance and the weapon's default stance, as the secondary button is pressed/released.

local WEAPON = script:FindAncestorByType('Weapon')
local ACTION_NAME = "Aim"
local ACTIVE_STANCE = "1hand_melee_shield_block"

function EnableStance(player)
    if Object.IsValid(player) and player == WEAPON.owner then
        player.animationStance = ACTIVE_STANCE
    end
end

function DisableStance(player)
    if WEAPON and Object.IsValid(player) then
        player.animationStance = WEAPON.animationStance
    end
end

function OnActionPressed(player, actionName)
    if actionName == ACTION_NAME then
        EnableStance(player)
    end
end

function OnActionReleased(player, actionName)
    if actionName == ACTION_NAME then
        DisableStance(player)
    end
end

function OnPlayerDied(player, damage)
    DisableStance(player)
end

function OnEquipped(weapon, player)
    Input.actionPressedEvent:Connect(OnActionPressed)
    Input.actionReleasedEvent:Connect(OnActionReleased)
end

WEAPON.equippedEvent:Connect(OnEquipped)

See also: Equipment.equippedEvent | Player.animationStance | Input.actionPressedEvent | CoreObject.FindAncestorByType | Object.IsValid | Event.Connect


Example using:

attackCooldownDuration

multiShotCount

burstCount

shotsPerSecond

The following function approximates a weapon's effective damage per second (DPS).

local WEAPON = script:FindAncestorByType("Weapon")

function ComputeDPS(weapon)
    local dps = 6.2
    while weapon.shotsPerSecond < dps do
        dps = dps / 2
    end

    local burst = math.max(1, weapon.burstCount)
    if burst < weapon.maxAmmo or weapon.maxAmmo <= 0 then
        local burstPeriod = (burst / dps + weapon.attackCooldownDuration)
        dps = burst / burstPeriod
    end

    return dps * weapon.damage * weapon.multiShotCount
end

print("DPS = " .. ComputeDPS(WEAPON))

See also: Weapon.maxAmmo | CoreObject.FindAncestorByType | CoreLua.print


Example using:

currentAmmo

maxAmmo

This script plays audio to the weapon owner when the weapon reaches 20% amount of ammo. It works best if the script is in a client context under the weapon, that way the audio is heard only by the player who is using the weapon.

local WEAPON = script:FindAncestorByType('Weapon')
local SHOOT_ABILITY = script:GetCustomProperty("ShootAbility"):WaitForObject()
local LOW_AMMO_SOUND = WEAPON:GetCustomProperty("LowAmmoSound")
local LOW_AMMO_PERCENTAGE = 0.2

function OnShootExecute(ability)
    if Object.IsValid(WEAPON) and ability.owner == WEAPON.owner then
        if WEAPON.currentAmmo / WEAPON.maxAmmo <= LOW_AMMO_PERCENTAGE then
            if LOW_AMMO_SOUND then
                World.SpawnAsset(LOW_AMMO_SOUND, {position = WEAPON:GetWorldPosition()})
            end
        end
    end
end

SHOOT_ABILITY.executeEvent:Connect(OnShootExecute)

See also: Equipment.owner | Ability.owner | CoreObject.FindAncestorByType | CoreObjectReference.WaitForObject | Object.IsValid | World.SpawnAsset | Event.Connect


Example using:

isAmmoFinite

reloadSoundId

While various properties are read-only, they are still useful in determining what behavior should occur, leading to more general purpose scripts. In this example, a script controls auto-reloading of weapons. It expects to be in a client context, because the ability's Activate() function is client-only.

local WEAPON = script:FindAncestorByType('Weapon')

local RELOAD_ABILITY = nil
-- Grabs reload ability from the weapon. Keep trying in case the client hasn't loaded the object yet
while not Object.IsValid(RELOAD_ABILITY) do
    Task.Wait()
    RELOAD_ABILITY = WEAPON:GetAbilities()[2]
end
-- The client script can now keep going, after it has acquired a reference to the reload ability
-- The above could also have been implemented with a :GetCustomProperty(...):WaitForObject()

-- Manually spawn the reloading audio
function SpawnReloadingAudio()
    if WEAPON.reloadSoundId ~= nil then
        World.SpawnAsset(WEAPON.reloadSoundId, {position = WEAPON:GetWorldPosition()})
    end
end

function Tick(deltaTime)

    -- Makes sure that the weapon owner is the local player
    if not Object.IsValid(WEAPON) then return end
    if not WEAPON.owner == Game.GetLocalPlayer() then return end

    if not WEAPON.isAmmoFinite then
        -- Checks when the weapon has empty ammo to reload
        if WEAPON.currentAmmo == 0 then
            SpawnReloadingAudio()
            RELOAD_ABILITY:Activate()
            Task.Wait(RELOAD_ABILITY.castPhaseSettings.duration)
        end
    end
end

See also: Weapon.GetAbilities | Equipment.owner | Ability.Activate | AbilityPhaseSettings.duration | Game.GetLocalPlayer | CoreObject.FindAncestorByType | World.SpawnAsset | Object.IsValid | Task.Wait | CoreLua.Tick


Example using:

isHitscan

range

damage

projectileTemplateId

trailTemplateId

impactSurfaceTemplateId

impactProjectileTemplateId

impactPlayerTemplateId

projectileSpeed

projectileLifeSpan

projectileGravity

projectileLength

projectileRadius

projectileDrag

This script implements a Wall Bang mechanic, allowing shots to go through walls.

Configuring walls/objects to be penetrable: For each wall or object that should be penetrable, add to them the "WallBang" custom property (float). Only objects with this property will be penetrable. Higher values mean the wall reduces more damage from shots that go through them. Objects with a "WallBang" value of 0 will let shots through but will not affect the damage amount.

Configuring this script on a weapon: Set this script's "WallBang" property to affect the weapon's penetrability when it's compared against objects. A higher value means it penetrates tougher walls and takes less damage reduction. A weapon can further control how much of its damage is reduced by setting the "DamageReduction" property on this script (Between zero and 1).

local WEAPON = script:FindAncestorByType("Weapon")
local WALL_BANG = script:GetCustomProperty("WallBang") or 2
local DAMAGE_REDUCTION = script:GetCustomProperty("DamageReduction") or 1

if WALL_BANG <= 0 then return end

function OnTargetImpactedEvent(weapon, impactData)
    if not Object.IsValid(weapon) then return end

    local wall = impactData.targetObject
    if not wall or not wall:IsA("StaticMesh") then return end

    -- If the wall hasn't defined the WallBang property it's impenetrable
    local wallBangResistance = wall:GetCustomProperty("WallBang")
    if not wallBangResistance or wallBangResistance >= WALL_BANG then return end

    -- Calculate damage
    local damage = weapon.damage
    if DAMAGE_REDUCTION > 0 then
        local percent = (WALL_BANG - wallBangResistance) / WALL_BANG
        damage = CoreMath.Lerp(0, weapon.damage, percent)

        percent = CoreMath.Clamp(DAMAGE_REDUCTION)
        damage = CoreMath.Lerp(weapon.damage, damage, percent)
    end

    -- Gather info about position and direction of the shot
    local impactPos = impactData:GetHitResult():GetImpactPosition()
    local direction = impactPos - weapon:GetWorldPosition()
    local remainingTravel = weapon.range - impactData.travelDistance

    -- Perhaps do more if the weapon is of hitscan type
    if not weapon.isHitscan then
        if impactData.projectile then
            direction = impactData.projectile:GetVelocity()
        end
    end
    direction = direction:GetNormalized()

    -- Do a series of raycasts to figure out where is the bullet's exit point
    local rayStart = impactPos + direction * 5
    local rayEnd = rayStart + direction * remainingTravel
    local rayParams = {}

    if Object.IsValid(impactData.weaponOwner) and impactData.weaponOwner.team > 0 then
        rayParams.ignoreTeams = weapon.owner.team
    end

    local hit = World.Raycast(rayStart, rayEnd, rayParams)
    if hit then
        rayEnd = rayStart
        rayStart = hit:GetImpactPosition()
    else
        local swapValue = rayEnd
        rayEnd = rayStart
        rayStart = swapValue
    end
    -- The 'hitInverted' is the info about the bullet's exit point
    local hitInverted = World.Raycast(rayStart, rayEnd, rayParams)
    if not hitInverted then return end

    -- Spawn the surface impact VFX on the opposite side of the object
    if weapon.impactSurfaceTemplateId then
        local t = hitInverted:GetTransform()
        SpawnVfx(weapon.impactSurfaceTemplateId, t:GetPosition(), t:GetRotation())
    end

    -- Spawn a new projectile to continue on the trajectory
    local projLength = 5 + weapon.projectileLength + weapon.projectileRadius
    startPos = hitInverted:GetImpactPosition() + direction * projLength
    local projectile = Projectile.Spawn(weapon.projectileTemplateId, startPos, direction)
    -- Copy properties from the weapon to the new projectile
    projectile.owner = impactData.weaponOwner
    projectile.sourceAbility = impactData.sourceAbility
    projectile.speed = weapon.projectileSpeed
    projectile.gravityScale = weapon.projectileGravity
    projectile.drag = weapon.projectileDrag
    projectile.lifeSpan = weapon.projectileLifeSpan * remainingTravel / weapon.range
    projectile.capsuleLength = weapon.projectileLength
    projectile.capsuleRadius = weapon.projectileRadius
    -- If some weapon properties are needed later it's safer to stash them in serverUserData,
    -- because the weapon might be destroyed while the projectile is still in the air:
    projectile.serverUserData.impactSurfaceTemplateId = weapon.impactSurfaceTemplateId
    projectile.serverUserData.impactPlayerTemplateId = weapon.impactPlayerTemplateId
    projectile.serverUserData.impactProjectileTemplateId = weapon.impactProjectileTemplateId
    projectile.serverUserData.direction = direction
    -- Store damage calculation onto the projectile because there may be multiple ones
    projectile.serverUserData.damage = damage

    -- Listen for the impact, to spawn effects and apply damage
    projectile.impactEvent:Connect(OnProjectileImpacted)

    -- Spawn a trail to follow the projectile
    if weapon.trailTemplateId and projectile.speed > 0 then
        local pos = hitInverted:GetImpactPosition()
        local trailLifeSpan = (rayStart - pos).size / projectile.speed
        trailLifeSpan = math.min(projectile.lifeSpan, trailLifeSpan)
        if trailLifeSpan > 0 then
            local rot = Rotation.New(direction, Vector3.UP)
            local trail = World.SpawnAsset(weapon.trailTemplateId, {position = pos, rotation = rot})
            trail:MoveContinuous(direction * projectile.speed)
            trail.lifeSpan = trailLifeSpan
        end
    end
end

function OnProjectileImpacted(projectile, other, hitResult)
    if not Object.IsValid(projectile) then return end

    local impactTemplate = nil

    if other:IsA("Player") then
        -- Construct and apply damage to player
        local dmg = Damage.New(projectile.serverUserData.damage)
        dmg.reason = DamageReason.COMBAT
        dmg:SetHitResult(hitResult)
        dmg.sourceAbility = projectile.sourceAbility
        dmg.sourcePlayer = projectile.owner
        other:ApplyDamage(dmg)

        impactTemplate = projectile.serverUserData.impactPlayerTemplateId
    else
        impactTemplate = projectile.serverUserData.impactSurfaceTemplateId
       end

    -- Spawn impact VFX
    local t = hitResult:GetTransform()
    if impactTemplate then
        SpawnVfx(impactTemplate, t:GetPosition(), t:GetRotation())
    end

    impactTemplate = projectile.serverUserData.impactProjectileTemplateId
    if impactTemplate then
        local rot = Rotation.New(projectile.serverUserData.direction, Vector3.UP)
        SpawnVfx(impactTemplate, t:GetPosition(), rot)
    end
end

function SpawnVfx(template, pos, rot)
    local vfx = World.SpawnAsset(template, {position = pos, rotation = rot})
    if vfx.lifeSpan <= 0 then
        vfx.lifeSpan = 1.2
    end
end

WEAPON.targetImpactedEvent:Connect(OnTargetImpactedEvent)

See also: Weapon.targetImpactedEvent | World.Raycast | ImpactData.targetObject | HitResult.GetImpactPosition | Projectile.Spawn | Damage.New | Player.team | CoreObject.FindAncestorByType | Object.serverUserData | other.isA | CoreMath.Lerp | Vector3.GetNormalized | Rotation.New | Transform.GetPosition | Event.Connect


Example using:

muzzleFlashTemplateId

This sample demonstrates several things. First, it creates a copy of the weapon's muzzle flash effect and attaches it to where the script is. Then, it shows how to traverse an object's hierarchy and create a custom table of objects to operate upon later--in this case it's trying to find smart objects that have both the Stop() and Play() functions. Finally, It shows how sound and VFX from a single spawned template can be played and stopped randomly. In other words, they are reused without having to spawn a new copy of the template each time.

local WEAPON = script:FindAncestorByType("Weapon")

if WEAPON.muzzleFlashTemplateId == nil then return end

local smartObjects = {}

local muzzleInstance = World.SpawnAsset(WEAPON.muzzleFlashTemplateId, {parent = script})

-- A utility function that runs the same operation on all nodes in an object's hierarchy
function ForEachChild(coreObj, functionToCall)
    functionToCall(coreObj)

    for _, child in ipairs(coreObj:GetChildren()) do
        ForEachChild(child, functionToCall)
    end
end

-- Find all the core objects in the template that have both the Play() and Stop() functions
ForEachChild(muzzleInstance, function(coreObj)
    if coreObj.Play and coreObj.Stop then
        table.insert(smartObjects, coreObj)
        coreObj:Stop()
    end
end)

while true do
    -- Wait between 0 and 1 second
    Task.Wait(math.random())

    -- Play all the effects
    for _, obj in ipairs(smartObjects) do
        obj:Play()
    end

    -- Wait between 0 and 0.3 seconds
    Task.Wait(math.random() * 0.3)

    -- Stop all the effects
    for _, obj in ipairs(smartObjects) do
        obj:Stop()
    end
    -- Repeat...
end

See also: CoreObject.FindAncestorByType | World.SpawnAsset | Vfx.Play | SmartAudio.Play | Task.Wait


Example using:

outOfAmmoSoundId

Weapons are also of type Equipment. In this example we listen to when a player equips the weapon. When they do, if the weapon is out of ammo then we play the "out of ammo" sound effect which normally only plays after trying to shoot while empty.

local WEAPON = script:FindAncestorByType('Weapon')

function OnEquipped(equipment, player)
    if WEAPON.currentAmmo == 0 and WEAPON.outOfAmmoSoundId then
        local pos = WEAPON:GetWorldPosition()
        World.SpawnAsset(WEAPON.outOfAmmoSoundId, {position = pos})
    end
end

WEAPON.equippedEvent:Connect(OnEquipped)

See also: Equipment.equippedEvent | CoreObject.FindAncestorByType | World.SpawnAsset | Event.Connect


Example using:

projectileBounceCount

projectilePierceCount

A weapon-viewing interface can show detailed specs about each weapon to players. In this example, the weapon's damage, as well as indicators if the shots bounce or pierce are setup for the player to view. This script would exist as part of a greater user interface, with various images and texts, and the ShowUI() function would be called depending on the game state (for example the player is browsing a shop).

local WEAPON_DETAILS_UI = script.parent
local DAMAGE_LABEL = script:GetCustomProperty("DamageLabel"):WaitForObject()
local BOUNCE_UI = script:GetCustomProperty("BounceGroup"):WaitForObject()
local PIERCE_UI = script:GetCustomProperty("PierceGroup"):WaitForObject()

function ShowUI(weapon)
    WEAPON_DETAILS_UI.visibility = Visibility.INHERIT

    -- Damage
    DAMAGE_LABEL.text = "Damage: " .. tostring(weapon.damage)

    -- Bounces? yes/no
    if weapon.projectileBounceCount > 0 then
        BOUNCE_UI.visibility = Visibility.INHERIT
    else
        BOUNCE_UI.visibility = Visibility.FORCE_OFF
    end

    -- Pierces? yes/no
    if weapon.projectilePierceCount > 0 then
        PIERCE_UI.visibility = Visibility.INHERIT
    else
        PIERCE_UI.visibility = Visibility.FORCE_OFF
    end
end

function HideUI()
    WEAPON_DETAILS_UI.visibility = Visibility.FORCE_OFF
end

See also: Weapon.damage | CoreObject.parent | CoreObjectReference.WaitForObject | UIText.text


Example using:

shouldBurstStopOnRelease

The following function evaluates a weapon and returns the "type" of weapon it thinks it is, based on some of its properties.

WeaponClass.AutomaticRifle = 1
WeaponClass.BurstRifle = 2
WeaponClass.Sniper = 3
WeaponClass.Pistol = 4
WeaponClass.Shotgun = 5

function ClassifyWeapon(weapon)
    if weapon.burst > 1 then
        if weapon.shouldBurstStopOnRelease then
            return WeaponClass.AutomaticRifle
        else
            return WeaponClass.BurstRifle
        end
    else
        if weapon.multiShotCount > 1 then
            return WeaponClass.Shotgun

        elseif string.match(weapon:GetAbilities()[1].animation, "pistol") then
            return WeaponClass.Pistol
        else
            return WeaponClass.Sniper
        end
    end
end

See also: Equipment.GetAbilities | Ability.animation


Example using:

spreadMin

spreadMax

spreadAperture

spreadDecreaseSpeed

spreadIncreasePerShot

spreadPenaltyPerShot

It can be hard to understand the implications of spread on the efficacy of a weapon, especially as there is a complex relationship with firing rate. This example demonstrates a data-driven approach to studying gameplay. If this script is added to each weapon they will register their stats into a global table, which could then be analyzed to draw conclusions about the game's balance.

local WEAPON = script:FindAncestorByType("Weapon")

if _G.WeaponStudy == nil then
    _G.WeaponStudy = {}
    _G.WeaponStudy.samples = {}

    _G.WeaponStudy.AddSample = function(weapon)
        local key = weapon.name
        if _G.WeaponStudy.samples[key] then return end

        _G.WeaponStudy.samples[key] = {
            spreadMin = weapon.spreadMin,
            spreadMax = weapon.spreadMax,
            spreadAperture = weapon.spreadAperture,
            spreadDecreaseSpeed = weapon.spreadDecreaseSpeed,
            spreadPenaltyPerShot = weapon.spreadPenaltyPerShot,
            damageDealt = 0
        }
    end

    _G.WeaponStudy.ReportDamage = function(weapon, amount)
        local key = weapon.name
        local dmg = _G.WeaponStudy.samples[key].damageDealt
        _G.WeaponStudy.samples[key].damageDealt = dmg + amount
    end
end

_G.WeaponStudy.AddSample(WEAPON)

local GOOD = true
local BAD = false

function CalcStatRating(statName, compareTo, goodOrBad)
    local minValue
    local maxValue
    for weaponName,data in pairs(_G.WeaponStudy.samples) do
        local statValue = data[statName]
        if not minValue or statValue < minValue then
            minValue = statValue
        end
        if not maxValue or statValue > maxValue then
            maxValue = statValue
        end
    end
    if not minValue then
        error("Missing samples")
        return
    end

    local result
    if maxValue == minValue then
        -- Avoid division by zero
        result = 1
    else
        result = (compareTo - minValue) / (maxValue - minValue)
    end

    if goodOrBad == GOOD then
        return result
    end
    return 1 - result
end

function RateMyGun()
    local spreadMinRating = CalcStatRating("spreadMin", WEAPON.spreadMin, BAD)
    local spreadMaxRating = CalcStatRating("spreadMax", WEAPON.spreadMax, BAD)
    local spreadApertureRating = CalcStatRating("spreadAperture", WEAPON.spreadAperture, BAD)
    local spreadDecreaseSpeedRating = CalcStatRating("spreadDecreaseSpeed", WEAPON.spreadDecreaseSpeed, GOOD)
    local spreadPenaltyPerShotRating = CalcStatRating("spreadPenaltyPerShot", WEAPON.spreadPenaltyPerShot, BAD)
    print("")
    print("Rating - " .. WEAPON.name)
    print("  spreadMin: " .. tostring(spreadMinRating))
    print("  spreadMax: " .. tostring(spreadMaxRating))
    print("  spreadAperture: " .. tostring(spreadApertureRating))
    print("  spreadDecreaseSpeed: " .. tostring(spreadDecreaseSpeedRating))
    print("  spreadPenaltyPerShot: " .. tostring(spreadPenaltyPerShotRating))
end

-- We're keeping track of damage dealt here, but additional study is needed to draw conclusions.
-- Plus, this would need to be tested in multiplayer and the data accessed somehow.
function OnTargetImpacted(weapon, impactData)
    if ImpactData.targetObject and ImpactData.targetObject:IsA("Player") then
        -- The damage calculation may change per weapon. This is a generic example
        local damageAmount = weapon.damage

        _G.WeaponStudy.ReportDamage(WEAPON, damageAmount)
    end
end

WEAPON.targetImpactedEvent:Connect(OnTargetImpacted)

-- Rate each gun based on how it measures against all the other ones in the hierarchy.
-- Works best if there are several guns in the scene, with a varying spread of stats.
Task.Wait(1)
RateMyGun()

See also: Weapon.targetImpactedEvent | ImpactData.targetObject | CoreObject.FindAncestorByType | CoreLua.print | other.IsA | Event.Connect | Task.Wait


Example using:

spreadMin

spreadMax

spreadDecreaseSpeed

Often in shooting games, the weapon loses precision while moving. For weapons in Core this is achieved by modifying the player's spreadModifier property, and can be implemented in many different ways. In this example, a client-context script uses the weapon's configured spreadMin and spreadMax properties to determine the maximum penalty when the player is moving. The weapon's spreadDecreaseSpeed is then used as an interpolation coefficient to smoothly move the spread penalty up and down, non-linearly, as the player moves or stops moving.

local WEAPON = script:FindAncestorByType("Weapon")
local MOVING_THRESHOLD = 250

local wasMoving = false
local targetSpreadModifier = 0

function Tick()
    local player = WEAPON.owner
    if not Object.IsValid(player) then return end

    -- Evaluate if the player is moving right now
    local isMovingNow = false
    if player.isJumping then
        isMovingNow = true
    else
        local playerSpeed = player:GetVelocity().size
        if playerSpeed >= MOVING_THRESHOLD then
            isMovingNow = true
        end
    end

    -- Select target spread modifier based on current movement
    if isMovingNow ~= wasMoving then
        if isMovingNow then
            -- Moving
            targetSpreadModifier = WEAPON.spreadMax - WEAPON.spreadMin
        else
            -- Not moving
            targetSpreadModifier = 0
        end
    end
    wasMoving = isMovingNow

    -- Adjust the player spread modify gradually over time
    local t = WEAPON.spreadDecreaseSpeed / 100
    player.spreadModifier = CoreMath.Lerp(player.spreadModifier, targetSpreadModifier, t)
end

See also: Equipment.owner | Player.spreadModifier | CoreObject.FindAncestorByType | Object.IsValid | Vector3.size | CoreLua.Tick | CoreMath.Lerp


Tutorials

Weapons in Core | Weapons & Abilities (Advanced)


Last update: April 12, 2022