• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

Railgun

Railgun spell - used to shoot a beam and damage units in a line.
Does damage depending on target's max health and depending on how close the target was to the center of the beam.
JASS:
library Railgun requires TimerUtils
//==================================================================================================
//---------------------------------Railgun-----by-Insanity_AI---------------------------------------
//==================================================================================================
/*/*    DISCLAIMER:         This spell visually works better on 1.30 PTR                        */*/
/*/*                  Primarily because of BlzGetLocalUnitZ() working properly there...         */*/
//
//  Setup:  Create a Railgun ability and modify trigger spell condition.
//          Create a dummy unit for trajectory visualizaton.
//          Make sure to match those objectIDs with configuration settings in here:
//          Primarily in SpellCondition() and in the Visuals category; VISUALIZATION_ID
/*          Make sure that you have a /*/*Unit Indexer*/*/ in your map.                     
*/
//  A bit of configuration info:
//          You can edit in the Configuration Section just down below.
//          Each configurable function or variable has a little comment describing what it does.
//          There are also functions that are responsible for damaging units, killing destructables,
//          And blacklisting them, those are all at your disposal.
//          I added a lot of visual configuration, I'm pretty sure you can use things like GetTriggerUnit()
//          in there, so I'm pretty sure that alone opens a lot of doors and posibilities.
//
//  About the damage:
//          At the moment it deals percentage of target's max health.
//          The damage is stacked on the target depending on how close they're to the beam,
//          which is determined by the RADIUS.
//          It goes up from 0% to 90%(in configuration) depending on how close the target is
//          to the beam.
//
//  Limitation:
//          It will shoot through terrain hills, so I'd suggest using some sort of pathingblockers
//          ... that is, unless you don't want this intended interaction (or lack of.)
//
//  Modifications:
//          Cast time, cooldown and mana cost is edited in the object editor,
//          as for the other things, they're editable right here.
//
//==================================================================================================
    globals
        private real        array   angle
        private real        array   offsetZ
        private group       array   visualizers
        private lightning   array   beam
        private real        array   fadeOutDuration
        private real        array   fadeOutRate
        private real        array   fadeOutPeriod
        private boolean     array   stopPoint
 
        private trigger     cast                        = CreateTrigger()
        private trigger     stop                        = CreateTrigger()
        private trigger     fire                        = CreateTrigger()
        private boolexpr    destructFilter                = null
        private boolexpr    unitFilter                    = null
        private boolexpr    castFunction                = null
        private boolexpr    stopFunction                = null
        private boolexpr    fireFunction                = null
    endglobals
 
    /*-----------------------------------------------------------------------------*/
    /*========================Configuration Section================================*/
    /*-----------------------------------------------------------------------------*/
 
    //A little foreword; All of these functions have access to GetTriggerUnit() and GetSpellAbilityId(),
    //and with such, opens many customization options.
    //No need for have the library twice to make 2 different spells, is what I am saying.
    private function SpellCondition takes nothing returns boolean
        return (GetSpellAbilityId() == 'A000')
    endfunction
   
    private function GetRange takes nothing returns real
        //Range to where the railgun shoots
        return 4000.00
    endfunction

    private function GetRadius takes nothing returns real
        //Railgun's visualizer area check for destructables and units
        return 100.00
    endfunction

    private function GetBeamStepDistance takes nothing returns real
        //Distance-step to check for beam's object blockers
        //Eg. A tree.
        return 40.00
    endfunction
 
    /*/*/*----------------------------------------------------------------------*/*/*/
    /*/*/*===========================Visuals====================================*/*/*/
    /*/*/*----------------------------------------------------------------------*/*/*/
    globals
        //UnitID of Railgun Trajectory visualization
        private integer     VISUALIZATION_ID        = 'n000'   
        private integer     VISUALIZATION_PLAYER    = PLAYER_NEUTRAL_PASSIVE
        private string      VISUAL_HIT_MODEL        = "Objects\\Spawnmodels\\NightElf\\NECancelDeath\\NECancelDeath.mdl"
        //Aka, how many lightnings does 1 beam contain, stacked up on 1 another.
        private integer     MAXLIGHTNINGCOUNT       = 5
            //- After a lot of thinking, I decided to put this back into variable form, because having multiple spells
            //with different lightningcounts, would cause interference in the lightning array.
    endglobals
 
    private function FadeOut takes nothing returns boolean
        //Would you like the railgun beam to fade out rather than just dissapear
        return true
    endfunction
 
    private function GetBeamDuration takes nothing returns real
        //Time before the railgun beam is removed from the map
        //I should note that you shouldn't cast another beam from the same unit, until this time has passed
        //So, set spell cooldown accordingly
        return 0.2
    endfunction
 
    private function GetFadeOutPeriod takes nothing returns real
        //In what intervals will it apply the fade out rate
        return 0.04
    endfunction
 
    private function GetFadeOutRate takes nothing returns real
        //By how much does the beam fade out per period,
        //(remember lightning colour, including alpha channel, goes from 0.00 to 1.00)
        //also if duration is lower than period, the beam will just dissapear instead, regardless if you have FadeOut as true.
        return 0.2
    endfunction
 
    private function GetVisualizationDistance takes nothing returns real
        //Distance between each of the visualization lights
        return 266.00
    endfunction
 
    private function GetLightningName takes nothing returns string
        // Change beam's lightning model, would suggest vanilla DRAM, DRAL, DRAB and LEAS models
        //Other models don't retain a "beam" sort of structure.
        return "LEAS"
    endfunction
 
    private function SetBeamColor takes lightning beam returns nothing
        //The real values are by order: Red, Green, Blue, Alpha
        //And take values from 0.00  to 1.00, apparently.
        //Now this won't change the beam to actual color, but just reduce the amount of colour the lightning
        //model already has. If you're unsatisfied with the results this gives you, try a different model.
        call SetLightningColor(beam,1.00,1.00,0.75,1.00)
        set beam                                    = null
    endfunction

    private function GetLightningVisualHeightOffset takes nothing returns real
        //Visual Z offset for the lightning.
        return 60.00
    endfunction
 
    private function GetCastAnimation takes nothing returns string
    //Used when the caster is prepearing to fire
        return "stand ready"
    endfunction
 
    private function GetFireAnimation takes nothing returns string
    //Used when caster is firing
        return "spell"
    endfunction
 
    private function GetStandAnimation takes nothing returns string
    //Queued after firing
        return "stand"
    endfunction
 
    /*/*/*----------------------------------------------------------------------*/*/*/
    /*/*/*=====================End of Visuals===================================*/*/*/
    /*/*/*----------------------------------------------------------------------*/*/*/
 
    private function UnitBlacklist takes nothing returns boolean
        //Any unit or unit-type you want it to be immune to this? You can add it here.
        return true
    endfunction
   
    private function DamageVictims takes real squaredDistance returns nothing
    //Function responsible for damaging units, you can edit this however you'd like.
    //squaredDistance is the distance of the unit from the beam... squared.
        local unit target                       = GetEnumUnit()
        local unit caster                       = GetTriggerUnit()
        local real maxRangeSquared              = Pow(GetRadius(),2)
        local real damage
        if(target !=  caster)then
            set damage                          = (maxRangeSquared - squaredDistance)/maxRangeSquared
            if damage > 0.90 then
                set damage                      = 0.90
            endif
            set damage                          = damage * GetUnitState(target,UNIT_STATE_MAX_LIFE)
            call UnitDamageTarget(caster,target,damage,true,false,ATTACK_TYPE_CHAOS,DAMAGE_TYPE_UNKNOWN,WEAPON_TYPE_WHOKNOWS)
            call DestroyEffect(AddSpecialEffectTarget(VISUAL_HIT_MODEL,target,"chest"))
        endif
   
        set target                              = null
        set caster                              = null
    endfunction

    private function DestructableBlacklist takes nothing returns boolean
    //Any additions you'd like to add here?
        local integer   destructType            = GetDestructableTypeId(GetFilterDestructable())
   
        //if the destructable is alive and is not a pathblocker of any kind.
        if GetDestructableLife(GetFilterDestructable()) <= 0 then
            return false
        endif
        set stopPoint[GetUnitUserData(GetTriggerUnit())]          = true
        return not((destructType == 'YTlb') or (destructType == 'YTab') or (destructType == 'YTpb') or (destructType == 'YTfb'))
    endfunction

    private function DestructSearchAndDestroy takes nothing returns nothing
        //How destructables are being destroyed.
        call SetDestructableLife(GetEnumDestructable(), 0.00)
    endfunction
    /*-----------------------------------------------------------------------------*/
    /*===================End of configuration section==============================*/
    /*-----------------------------------------------------------------------------*/
 
      private function CheckUnitTarget takes nothing returns nothing
        local unit target                       = GetEnumUnit()
        local real targetX                      = GetUnitX(target)
        local real targetY                      = GetUnitY(target)
        local unit caster                       = GetTriggerUnit()
        local real casterX                      = GetUnitX(caster)
        local real casterY                      = GetUnitY(caster)
        local integer casterData                = GetUnitUserData(caster)
        local real maxRangeSquared              = Pow(GetRadius(),2)
        local real angleT                       = Atan2(targetY-casterY,targetX-casterX) - angle[casterData]
        local real targetDistance
   
        //Ignore units that are behind the caster.
        if not(angleT < bj_PI/2 and angleT > -bj_PI/2) then
            set target                          = null
            set caster                          = null
            return
        endif
   
        //A slightly modified Distance between a line and a point formula
        //Ax + By + C = 0 => -ax + y + b = 0 from y = ax - b, where a = tan(angle) and b = a(x1) - y1
        //Formula: |Ax + By + C|/Sqrt(A^2 + B^2)=> ((-ax + y + b)^2)/(a^2 + 1)
        set angleT                              = Tan(angle[casterData])
        set targetDistance                      = Pow(-angleT*targetX + targetY + angleT*casterX - casterY,2)/(Pow(angleT,2) + 1)
        if targetDistance > maxRangeSquared then
            set target                          = null
            set caster                          = null
            return
        endif
   
        //If it checks out, damage it
        call DamageVictims(targetDistance)
        set target                          = null
        set caster                          = null
    endfunction
 
    private function GetVisualizationStopIndex takes nothing returns integer
        return R2I(GetRange()/GetVisualizationDistance())
    endfunction
 
    private function GetBeamStopIndex takes nothing returns integer
        return R2I(GetRange()/GetBeamStepDistance())
    endfunction
   
    private function Cast takes nothing returns nothing
        local unit      caster                  = GetTriggerUnit()     
        local rect      destructCheckRect       = Rect(0,0,0,0)
        local unit      visualizer              = null
        local group     visualizerGroup         = CreateGroup()
        local real      casterX                 = GetUnitX(caster)
        local real      casterY                 = GetUnitY(caster)
        local real      casterZ                 = BlzGetLocalUnitZ(caster)
        local real      targetX                 = GetSpellTargetX()
        local real      targetY                 = GetSpellTargetY()
        local real      targetZ           
   
        local real      range                   = GetRange()
        local real      visualDistance          = GetVisualizationDistance()
        local real      radius                  = GetRadius()
   
        local integer   iteration               = 1
        local integer   iterationEnd            = GetVisualizationStopIndex()
        local integer   casterData              = GetUnitUserData(caster)
   
        local real      offsetX 
        local real      offsetY
   
        call SetUnitAnimation(caster, GetCastAnimation())
        set angle[casterData]                   = Atan2(targetY-casterY,targetX-casterX)
        set offsetX                             = visualDistance*Cos(angle[casterData])
        set offsetY                             = visualDistance*Sin(angle[casterData])
   
        set visualizers[casterData]             = CreateGroup()
        set stopPoint[casterData]               = false
        loop
            exitwhen iteration > iterationEnd
            set targetX                         = casterX + offsetX*I2R(iteration)
            set targetY                         = casterY + offsetY*I2R(iteration)
       
            set visualizer                      = CreateUnit(Player(VISUALIZATION_PLAYER),VISUALIZATION_ID,targetX,targetY,angle[casterData])
            call GroupAddUnit(visualizerGroup,visualizer)
       
            call SetRect(destructCheckRect,targetX-radius,targetY-radius,targetX+radius,targetY+radius)
            call EnumDestructablesInRect(destructCheckRect,destructFilter, null)
       
            exitwhen stopPoint[casterData]
            set iteration                       = iteration +1
        endloop
       
        set targetZ                             = BlzGetLocalUnitZ(visualizer)
        set offsetZ[casterData]                 = (targetZ - casterZ)/range
   
        set iteration                           = 1
        loop
            set visualizer                      = FirstOfGroup(visualizerGroup)
            exitwhen visualizer == null
       
            call UnitAddAbility(visualizer,'Amrf')
            call SetUnitFlyHeight(visualizer,offsetZ[casterData] * I2R(iteration) + casterZ - BlzGetLocalUnitZ(visualizer) ,0.00)
            call UnitRemoveAbility(visualizer,'Amrf')
       
            set iteration                       = iteration + 1
            call GroupRemoveUnit(visualizerGroup,visualizer)
            call GroupAddUnit(visualizers[casterData],visualizer)
        endloop
       
        call RemoveRect(destructCheckRect)
        call DestroyGroup(visualizerGroup)
   
        set visualizerGroup                     = null
        set visualizer                          = null
        set caster                              = null
        set destructCheckRect                   = null
    endfunction

    private function Stop takes nothing returns nothing
        //in case the caster stops casting the railgun
        local integer   casterData              = GetUnitUserData(GetTriggerUnit())
        local unit      dummy                   = null
   
        loop
            set dummy                           = FirstOfGroup(visualizers[casterData])
            exitwhen (dummy == null)
            call GroupRemoveUnit(visualizers[casterData],dummy)
            call RemoveUnit(dummy)
        endloop
        call DestroyGroup(visualizers[casterData])
        set dummy                               = null
        set visualizers[casterData]             = null
    endfunction

    private function FireCleanup takes nothing returns nothing
        local timer     t                       = GetExpiredTimer()
        local integer   casterData              = GetTimerData(t)
        local integer   iteration               = 0
        local integer   endIteration            = MAXLIGHTNINGCOUNT
   
        loop
            exitwhen iteration >= endIteration
            call DestroyLightning(beam[casterData*endIteration + iteration])
            set iteration                       = iteration + 1
        endloop
   
        call ReleaseTimer(t)
        set t                                   = null
    endfunction
 
    private function FireFadeOut takes nothing returns nothing
        local timer     t                       = GetExpiredTimer()
        local integer   casterData              = GetTimerData(t)
        local integer   iteration               = 0
        local integer   endIteration            = MAXLIGHTNINGCOUNT
        local lightning localBeam               = null
   
        set fadeOutDuration[casterData]         = fadeOutDuration[casterData] - fadeOutPeriod[casterData]
   
        if fadeOutDuration[casterData] <= 0.00 then
            call PauseTimer(t)
            call FireCleanup()
        endif
   
        loop
            exitwhen iteration >= endIteration
            set localBeam                       = beam[casterData*endIteration+iteration]
            call SetLightningColor(localBeam, GetLightningColorR(localBeam),GetLightningColorG(localBeam),GetLightningColorB(localBeam),GetLightningColorA(localBeam)- fadeOutRate[casterData])
            set iteration                       = iteration + 1
        endloop
   
        set t                                   = null
        set localBeam                           = null
    endfunction

    private function Fire takes nothing returns nothing
        local unit      caster                  = GetTriggerUnit()
        local integer   casterData              = GetUnitUserData(caster)
        local timer     t                       = NewTimerEx(casterData)
        local rect      multipurposeRect        = Rect(0,0,0,0)
        local group     victims                 = CreateGroup()
   
        local real      lightningZOffset        = GetLightningVisualHeightOffset()
   
        local real      casterX                 = GetUnitX(caster)
        local real      casterY                 = GetUnitY(caster)
        local real      casterZ                 = BlzGetLocalUnitZ(caster) + lightningZOffset
        local real      stepDistance            = GetBeamStepDistance()
        local real      radius                  = GetRadius()
        local string    lightningName           = GetLightningName()
        local real      offsetX                 = stepDistance*Cos(angle[casterData])
        local real      offsetY                 = stepDistance*Sin(angle[casterData])
   
        local real      rectX               
        local real      rectY
   
        local integer   iteration               = 0
        local integer   endIteration            = MAXLIGHTNINGCOUNT
   
        local real      targetX
        local real      targetY
        local real      targetZ
   
        set stopPoint[casterData]               = false
   
        //visual appeal
        call SetUnitAnimation(caster, GetFireAnimation())
        call QueueUnitAnimation(caster, GetStandAnimation())
   
        loop
            exitwhen iteration >= endIteration
            set beam[casterData*endIteration + iteration] = AddLightningEx(lightningName ,true ,casterX,casterY ,0.00 ,casterX + 1.00,casterY + 1.00,0.00)
       
            set iteration                       = iteration + 1
        endloop
   
        set iteration                           = 1
        set endIteration                        = GetBeamStopIndex()
        loop
            exitwhen iteration >= endIteration
            set targetX                         = casterX + offsetX*I2R(iteration)
            set targetY                         = casterY + offsetY*I2R(iteration)
       
            call SetRect(multipurposeRect,targetX-radius,targetY-radius,targetX+radius,targetY+radius)
            call EnumDestructablesInRect(multipurposeRect,destructFilter, function DestructSearchAndDestroy)
       
            exitwhen stopPoint[casterData]
            set iteration                       = iteration +1
        endloop
   
        //OffsetX/Y is no longer used, so I'll repurpose them here!
        set offsetX                             = targetX
        set offsetY                             = targetY
        set rectX                               = casterX
        set rectY                               = casterY
   
        if targetX > casterX then
            set offsetX                         = casterX
            set rectX                           = targetX
        endif
   
        if targetY > casterY then
            set offsetY                         = casterY
            set rectY                           = targetY
        endif
   
        call SetRect(multipurposeRect,offsetX-radius,offsetY-radius,rectX+radius,rectY+radius)
        call GroupEnumUnitsInRect(victims,multipurposeRect,unitFilter)
        call ForGroup(victims, function CheckUnitTarget)
   
        set targetZ                             = casterZ + offsetZ[casterData]*R2I(iteration)*stepDistance
        set iteration                           = 0
        set endIteration                        = MAXLIGHTNINGCOUNT
   
        loop
            exitwhen iteration >= endIteration
            call MoveLightningEx(beam[casterData*endIteration + iteration],true,targetX,targetY,targetZ,casterX,casterY,casterZ)
            call SetBeamColor(beam[casterData*endIteration + iteration])
            set iteration                       = iteration + 1
        endloop
   
        if FadeOut() then
            set fadeOutDuration[casterData]     = GetBeamDuration()
            set fadeOutRate[casterData]         = GetFadeOutRate()
            set fadeOutPeriod[casterData]       = GetFadeOutPeriod()
            call TimerStart(t,GetFadeOutPeriod(),true,function FireFadeOut)
        else
            call TimerStart(t,GetBeamDuration(),false,function FireCleanup)
        endif
   
        call DestroyGroup(victims)
        call RemoveRect(multipurposeRect)
        set caster                              = null
        set multipurposeRect                    = null
        set victims                             = null
        set t                                   = null
    endfunction
 
    private function Cast_Wrapper takes nothing returns boolean
        if SpellCondition() then
            call Cast()
            return true
        endif
        return false
    endfunction

    private function Stop_Wrapper takes nothing returns boolean
        if SpellCondition() then
            call Stop()
            return true
        endif
        return false
    endfunction

    private function Fire_Wrapper takes nothing returns boolean
        if SpellCondition() then
            call Fire()
            return true
        endif
        return false
    endfunction
    //================================================================================
    function InitTrig_Railgun takes nothing returns nothing
   
        //this is so that the system knows when to stop the railgun. Value depends on 3 pre-set constants.
        set destructFilter                      = Filter(function DestructableBlacklist)
        set unitFilter                          = Filter(function UnitBlacklist)
   
        set castFunction                        = Condition(function Cast_Wrapper)
        set stopFunction                        = Condition(function Stop_Wrapper)
        set fireFunction                        = Condition(function Fire_Wrapper)
   
        //Cast the Railgun
        call TriggerRegisterAnyUnitEventBJ(cast, EVENT_PLAYER_UNIT_SPELL_CHANNEL)
        call TriggerAddCondition(cast, castFunction)
   
        //Stop the Railgun
        call TriggerRegisterAnyUnitEventBJ(stop, EVENT_PLAYER_UNIT_SPELL_ENDCAST )
        call TriggerAddCondition(stop, stopFunction)
   
        //Be the... no wait, Fire the Railgun
        call TriggerRegisterAnyUnitEventBJ(fire, EVENT_PLAYER_UNIT_SPELL_FINISH)
        call TriggerAddCondition(fire, fireFunction)
   
        call Preload(VISUAL_HIT_MODEL)
    endfunction
endlibrary
Lua:
if Debug then Debug.beginFile "Railgun" end
OnInit.module("Railgun", function(require)
    --==================================================================================================
    -----------------------------------Railgun-----by-Insanity_AI---------------------------------------
    --==================================================================================================
    -- Requirements:
    -- Total_Initialization -- https://www.hiveworkshop.com/threads/total-initialization.317099/post-3641920
    require "SetUtils"      -- https://www.hiveworkshop.com/threads/setutils.353716
    require "TimerQueue"    -- https://www.hiveworkshop.com/threads/timerqueue-stopwatch.353718
    require "ApplyOverTime" -- found in Dependencies
    require "HandleType"    -- https://www.hiveworkshop.com/threads/get-handle-type.354436
    --  since Railgun is defined as a module, you must place `require "Railgun"` somewhere
    --
    --  About the damage:
    --          At the moment it deals percentage of target's max health.
    --          The damage is stacked on the target depending on how close they're to the beam,
    --          which is determined by the RADIUS.
    --          It goes up from 0% to 90%(in configuration) depending on how close the target is
    --          to the beam.
    --
    --  Limitation:
    --          It will shoot through terrain hills, so I'd suggest using some sort of pathingblockers
    --          ... that is, unless you don't want this intended interaction (or lack of.)
    --
    --  API:
    --   Note: arguments denoted with '?' are optional; can be nilled
    --
    --   Railgun.create(range, aimVisualRadius, aimVisualStepDelta, beamStepDelta, beamWidth, unitFilter?, destructableFilter?,
    --          beamConstructor?, beamDestructor?, targetHandler?, aimVisualConstructor?, aimVisualDestructor?) -> Railgun instance
    --    - range: number - distance the beam travels
    --    - aimVisualRadius: number - radius the aiming visualizer uses with destructableFilter to check for when to stop
    --    - aimVisualStepDelta: number - distance between visualizers
    --    - beamStepDelta: number - distance between points for when beam fires and checks for obstacles
    --    - beamWidth: number - width of the beam used for obstacle check and target damage
    --    - unitFilter?: filterfunc - filter function that checks for eligible unit targets (no filter = any unit)
    --    - destructableFilter?: fitlerfunc - filter function that checks for eligible destructable targets/obstacles (no filter = alive destructables and not pathing blockers)
    --    - beamConstructor?: fun(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number) -> unknown
    --         -- expected function that returns something resembling a beam, can be effect or something else entirely, as long as you implement:
    --    - beamDestructor?: fun(beam: unknown) - used to destroy the custom beam
    --    - targetHandler?: fun(caster: unit, target: widget, squaredDistance: number, beamWidthSquared: number) - function that deals damage or whatever effect the user wants
    --    - aimVisualConstructor?: fun(x: number, y: number, z: number) -> unknown - similar with beam, except this is for creating visualizers that appear while aiming/casting
    --    - aimVisualDestructor?: fun(aimVisual: unknown) - similar with beam, except this is for removing visualizers that appear while aiming/casting
    --    -> returns a Railgun instance for your convenience.
    --
    --  Railgun:removeAimVisuals() - removes all aim visuals
    --  Railgun:aim(casterX, casterY, casterZ, targetX, targetY, targetZ) -- creates aim visuals from caster to target positions, checking with destructableFilter for obstacles
    --  Railgun:fire(caster: unit, startX, startY, startZ, targetX, targetY, targetZ) -- spawns the beam, checking for units and destructables and dealing effects if any on its path
    --==================================================================================================

    local aot = ApplyOverTime.create(TimerQueue)
    local rect = Rect(0, 0, 0, 0)

    local defaultDestructFilter = Filter(function()
        --Any additions you'd like to add here?
        local destructType = GetDestructableTypeId(GetFilterDestructable())

        --if the destructable is alive and is not a pathblocker of any kind.
        if GetDestructableLife(GetFilterDestructable()) <= 0 then
            return false
        end
        return not ((destructType == 'YTlb') or (destructType == 'YTab') or (destructType == 'YTpb') or (destructType == 'YTfb'))
    end)

    local function hitMarker(x, y)
        DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\NightElf\\NECancelDeath\\NECancelDeath.mdl", x, y))
    end

    ---@param caster unit
    ---@param widget widget
    ---@param squaredDistance number
    ---@param beamWidthSquared number
    local function defaultTargetHandler(caster, widget, squaredDistance, beamWidthSquared)
        --Function responsible for damaging units, you can edit this however you'd like.
        --squaredDistance is the distance of the unit from the beam... squared.
        if (widget ~= caster) then
            local widgetType = HandleType[widget]
            if widgetType == 'unit' then
                if UnitAlive(widget --[[@as unit]]) then
                    local damage = (beamWidthSquared - squaredDistance) / beamWidthSquared
                    if damage > 0.90 then
                        damage = 0.90
                    end
                    damage = damage * GetUnitState(widget --[[@as unit]], UNIT_STATE_MAX_LIFE)
                    UnitDamageTarget(caster, widget, damage, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNKNOWN, WEAPON_TYPE_WHOKNOWS)
                    hitMarker(GetUnitX(widget --[[@as unit]]), GetUnitY(widget --[[@as unit]]))
                end
            elseif widgetType == 'destructable' then
                if GetDestructableLife(widget --[[@as destructable]]) > 0 then
                    SetDestructableLife(widget --[[@as destructable]], 0)
                    hitMarker(GetDestructableX(widget --[[@as destructable]]), GetDestructableY(widget --[[@as destructable]]))
                end
            else
                return -- ignore items
            end
        end
    end

    ---@alias Beam unknown
    ---@alias BeamVisualConstructor fun(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number): Beam

    ---@type BeamVisualConstructor
    local function defaultBeamConstructor(startX, startY, startZ, endX, endY, endZ)
        local beams = {}
        local lightning ---@type lightning
        for i = 1, 5 do
            lightning = AddLightningEx('LEAS', true, endX, endY, endZ + 60.0, startX, startY, startZ + 60.0)
            SetLightningColor(lightning, 1.00, 1.00, 0.75, 1.00)
            beams[i] = lightning
        end
        return beams
    end

    ---@param beam Beam
    local function defaultBeamDestructor(beam)
        aot:Builder()
            :addStaticParam(beam)
            :addVariable(1.0, 0.0, false)
        ---@type fun(thisBeam: lightning[], alpha: number)
            :execute(0.2, 0.04, function(thisBeam, alpha)
                for _, lightning in ipairs(thisBeam) do
                    SetLightningColor(lightning, GetLightningColorR(lightning), GetLightningColorG(lightning), GetLightningColorB(lightning), alpha)
                end
            end)
        TimerQueue:callDelayed(0.21, function(thisBeam)
            for index, lightning in ipairs(thisBeam) do
                thisBeam[index] = nil
                DestroyLightning(lightning)
            end
        end, beam)
    end

    ---@alias AimVisual unknown
    ---@alias AimVisualConstructor fun(x: number, y: number, z: number): AimVisual

    ---@type AimVisualConstructor
    local function defaultVisualConstructor(x, y, z)
        local visualizer = AddSpecialEffect("Abilities\\Weapons\\SpiritOfVengeanceMissile\\SpiritOfVengeanceMissile.mdl", x, y)
        BlzSetSpecialEffectZ(visualizer, z)
        return visualizer
    end

    ---@class Railgun
    ---@field package range number
    ---@field package aimVisualRadius number
    ---@field package aimVisualStepDelta number
    ---@field package beamStepDelta number
    ---@field package beamWidth number
    ---@field package unitFilter filterfunc?
    ---@field package destructableFilter filterfunc
    ---@field package aimVisuals AimVisual[]
    ---@field package beamConstructor BeamVisualConstructor
    ---@field package beamDestructor fun(beams: Beam)
    ---@field package targetHandler fun(caster: unit, target: widget, squaredDistance: number, beamWidthSquared: number)
    ---@field package aimVisualConstructor AimVisualConstructor
    ---@field package aimVisualDestructor fun(aimVisual: AimVisual)
    Railgun = {}
    Railgun.__index = Railgun

    ---@param range number
    ---@param aimVisualRadius number
    ---@param aimVisualStepDelta number
    ---@param beamStepDelta number
    ---@param beamWidth number
    ---@param unitFilter filterfunc?
    ---@param destructableFilter filterfunc?
    ---@param beamConstructor BeamVisualConstructor?
    ---@param beamDestructor fun(beams: Beam)?
    ---@param targetHandler fun(caster: unit, target: widget, squaredDistance: number, beamWidthSquared: number)?
    ---@param aimVisualConstructor AimVisualConstructor?
    ---@param aimVisualDestructor fun(aimVisual: AimVisual)?
    function Railgun.create(range, aimVisualRadius, aimVisualStepDelta, beamStepDelta, beamWidth, unitFilter, destructableFilter,
                            beamConstructor, beamDestructor, targetHandler, aimVisualConstructor, aimVisualDestructor)
        return setmetatable({
            range = range,
            aimVisualRadius = aimVisualRadius,
            aimVisualStepDelta = aimVisualStepDelta,
            beamStepDelta = beamStepDelta,
            beamWidth = beamWidth,
            unitFilter = unitFilter,
            aimVisuals = {},
            destructableFilter = destructableFilter or defaultDestructFilter,
            beamConstructor = beamConstructor or defaultBeamConstructor,
            beamDestructor = beamDestructor or defaultBeamDestructor,
            targetHandler = targetHandler or defaultTargetHandler,
            aimVisualConstructor = aimVisualConstructor or defaultVisualConstructor,
            aimVisualDestructor = aimVisualDestructor or DestroyEffect
        }, Railgun)
    end

    function Railgun:removeAimVisuals()
        for index, aimVisual in ipairs(self.aimVisuals) do
            self.aimVisualDestructor(aimVisual)
            self.aimVisuals[index] = nil
        end
    end

    ---@param casterX number
    ---@param casterY number
    ---@param casterZ number
    ---@param targetX number
    ---@param targetY number
    ---@param targetZ number
    function Railgun:aim(casterX, casterY, casterZ, targetX, targetY, targetZ)
        local angle = math.atan(targetY - casterY, targetX - casterX)
        local offsetX = self.aimVisualStepDelta * math.cos(angle)
        local offsetY = self.aimVisualStepDelta * math.sin(angle)
        local iterations = math.modf(self.range / self.aimVisualStepDelta)
        local offsetZ = (targetZ - casterZ) / self.range

        local tempX, tempY, tempZ = casterX, casterY, casterZ
        for _ = 1, iterations do
            tempX = tempX + offsetX
            tempY = tempY + offsetY
            tempZ = tempZ + offsetZ
            table.insert(self.aimVisuals, self.aimVisualConstructor(tempX, tempY, tempZ))

            SetRect(rect,
                tempX - self.aimVisualRadius,
                tempY - self.aimVisualRadius,
                tempX + self.aimVisualRadius,
                tempY + self.aimVisualRadius)
            if SetUtils.getDestructablesInRectMatching(rect, self.destructableFilter):size() > 0 then
                break;
            end
        end
    end

    ---@param tarX number
    ---@param tarY number
    ---@param startX number
    ---@param startY number
    ---@param angle number
    ---@return number
    local function distanceFromBeam(tarX, tarY, startX, startY, angle)
        --A slightly modified Distance between a line and a point formula
        --Ax + By + C = 0 => -ax + y + b = 0 from y = ax - b, where a = tan(angle) and b = a(x1) - y1
        --Formula: |Ax + By + C|/Sqrt(A^2 + B^2) => ((-ax + y + b)^2)/(a^2 + 1)
        local angleT = math.tan(angle)
        return ((-angleT * tarX + tarY + angleT * startX - startY) ^ 2) / ((angleT ^ 2) + 1)
    end

    ---@param caster unit
    ---@param startX number
    ---@param startY number
    ---@param startZ number
    ---@param targetX number
    ---@param targetY number
    ---@param targetZ number
    function Railgun:fire(caster, startX, startY, startZ, targetX, targetY, targetZ)
        local angle = math.atan(targetY - startY, targetX - startX)
        local offsetX, offsetY = self.beamStepDelta * math.cos(angle), self.beamStepDelta * math.sin(angle)
        local steps = math.modf(self.range / self.beamStepDelta)
        local offsetZ = 0 --(targetZ - startZ) / steps
        local maxRangeSquared = self.beamWidth ^ 2

        local i = 1
        targetX, targetY = startX, startY
        while i <= steps do
            targetX = targetX + offsetX
            targetY = targetY + offsetY
            SetRect(rect, targetX - self.beamWidth, targetY - self.beamWidth, targetX + self.beamWidth, targetY + self.beamWidth)
            local destructable = SetUtils.getDestructablesInRectMatching(rect, self.destructableFilter):random()
            if destructable ~= nil then
                local tarX   = GetDestructableX(destructable)
                local tarY   = GetDestructableY(destructable)
                local angleT = math.atan(tarY - startY, tarX - startX) - angle

                --Ignore units that are behind the caster.
                if angleT < bj_PI / 2 and angleT > -bj_PI / 2 then
                    local targetDistance = distanceFromBeam(tarX, tarY, startX, startY, angle)
                    if targetDistance <= maxRangeSquared then
                        self.targetHandler(caster, destructable, targetDistance, maxRangeSquared)
                        break
                    end
                end
            end

            i = i + 1
        end
        targetZ = startZ + offsetZ * i

        SetRect(rect,
            math.min(startX, targetX) - self.beamWidth,
            math.min(startY, targetY) - self.beamWidth,
            math.max(startX, targetX) + self.beamWidth,
            math.max(startY, targetY) + self.beamWidth)
        for target in SetUtils.getUnitsInRectMatching(rect, self.unitFilter):elements() do
            local tarX   = GetUnitX(target)
            local tarY   = GetUnitY(target)
            local angleT = math.atan(tarY - startY, tarX - startX) - angle

            --Ignore units that are behind the caster.
            if angleT < bj_PI / 2 and angleT > -bj_PI / 2 then
                local targetDistance = distanceFromBeam(tarX, tarY, startX, startY, angle)
                if targetDistance <= maxRangeSquared then
                    self.targetHandler(caster, target, targetDistance, maxRangeSquared)
                end
            end
        end

        self.beamDestructor(self.beamConstructor(startX, startY, startZ, targetX, targetY, targetZ))
    end
end)
if Debug then Debug.endFile() end
Lua:
if Debug then Debug.beginFile "RailgunAbil" end
OnInit.trig(function(require)
    --==================================================================================================
    --------------------------------Railgun Abil-----by-Insanity_AI-------------------------------------
    --==================================================================================================
    require "Railgun"
    require "GetPointZ" -- found in Dependencies

    --[[
        This is an example trigger for how you can setup Railgun ability, you can either use this or
        write your own logic that does not rely on unit abilities, if you so choose.
    ]]

    local ABILITY_ID = FourCC('A000')          -- Railgun ability code
    local RAILGUN_RANGE = 4000.00              -- Railgun maximum range
    local AIM_VISUAL_RADIUS = 100.00           -- Aim visualizers' obstacle checker radius
    local AIM_VISUAL_STEP_DELTA = 266.66       -- Distance between 2 aim visualizers
    local BEAM_STEP_DELTA = 40.00              -- Distance between points in the beam for obstacle checking
    local BEAM_WIDTH = 100.00                  -- Width of the beam for damaging targets and obstacle checking

    local CASTER_ANIMATION_AIM = "stand ready" -- casting spell animation
    local CASTER_ANIMATION_FIRE = "spell"      -- starts effect of spell animation
    local CASTER_ANIMATION_STAND = "stand"     -- default animation after spell completes

    -- Utility class to store target position on
    ---@class RailgunEx: Railgun
    ---@field targetX number
    ---@field targetY number
    ---@field targetZ number

    -- Overriding the default visualizer so that the effects do not get hidden underneath this uneven terrain
    ---@param x number
    ---@param y number
    ---@param z number
    ---@return effect
    local function visualizerConstructor(x, y, z)
        return AddSpecialEffect("Abilities\\Spells\\Undead\\AbsorbMana\\AbsorbManaBirthMissile.mdl", x, y)
    end

    local spellInstances = {} ---@type table<unit, RailgunEx>

    ---@param caster unit
    ---@param targetX number
    ---@param targetY number
    ---@param targetZ number
    ---@return Railgun
    local function getSpellInstance(caster, targetX, targetY, targetZ)
        local instance = spellInstances[caster]

        if instance ~= nil then
            instance:removeAimVisuals()
        else
            instance = Railgun.create(RAILGUN_RANGE, AIM_VISUAL_RADIUS, AIM_VISUAL_STEP_DELTA, BEAM_STEP_DELTA, BEAM_WIDTH, nil, nil, nil, nil, nil, visualizerConstructor, nil) --[[@as RailgunEx]]
            spellInstances[caster] = instance
        end

        instance.targetX = targetX
        instance.targetY = targetY
        instance.targetZ = targetZ
        return instance
    end

    local castTrigger = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(castTrigger, EVENT_PLAYER_UNIT_SPELL_CHANNEL)
    TriggerAddAction(castTrigger, function()
        if GetSpellAbilityId() ~= ABILITY_ID then return end

        local caster = GetTriggerUnit()

        local targetX, targetY = GetSpellTargetX(), GetSpellTargetY()
        local targetZ = GetPointZ(targetX, targetY)

        getSpellInstance(caster, targetX, targetY, targetZ):aim(
            GetUnitX(caster), GetUnitY(caster), BlzGetUnitZ(caster),
            targetX, targetY, targetZ
        )

        SetUnitAnimation(caster, CASTER_ANIMATION_AIM)
    end)

    local stopTrigger = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(stopTrigger, EVENT_PLAYER_UNIT_SPELL_ENDCAST)
    TriggerAddAction(stopTrigger, function()
        if GetSpellAbilityId() ~= ABILITY_ID then return end
        local caster = GetTriggerUnit()
        spellInstances[caster]:removeAimVisuals()
        spellInstances[caster] = nil
        SetUnitAnimation(caster, CASTER_ANIMATION_STAND)
    end)

    local fireTrigger = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(fireTrigger, EVENT_PLAYER_UNIT_SPELL_FINISH)
    TriggerAddAction(fireTrigger, function()
        if GetSpellAbilityId() ~= ABILITY_ID then return end
        local caster = GetTriggerUnit()
        local spell = spellInstances[caster]
        spell:fire(caster, GetUnitX(caster), GetUnitY(caster), BlzGetUnitZ(caster), spell.targetX, spell.targetY, spell.targetZ)
        SetUnitAnimation(caster, CASTER_ANIMATION_FIRE)
        QueueUnitAnimation(caster, CASTER_ANIMATION_STAND)
    end)
end)
if Debug then Debug.endFile() end

3.11.2025:
  • Lua version
11.9.2018:
  • Now no longer deals damage 5 times, but will deal damage only once, depending on target's distance from the beam
  • No longer spams GroupEnumUnitsInRange, but rather picks up all units in rect from caster to target (the rect is expanded in all 4 directions by Radius() )
  • System now uses a modified "Distance between point and line" equation, without square root
  • Now deals percentage damage from 0% to 90% depending on distance between target and the beam. (Special case: Ignores units that are caught by GroupEnumUnitsInRect(), but are behind the caster.)
  • Removed GetPointZ and GetUnitZ functions in favour to use BlzGetLocalUnitZ() native (doesn't work in 1.29)
  • Attempted to make the railgun's visualization dummies in a somewhat straight line(in terms of height) , will consider using SFX instead..
  • The system now no longer keeps creating and destroying rects in a loop, but rather creates 1 rect and re-uses it till the loop is done, get's removed at the end of the trigger.
3.9.2018:
  • Nulled a leftover local variable
  • Changes some function names to make more sense
  • Added more comments
2.9.2018:
  • Stumbled upon an idea; added fade-out functionality and made it an option.
  • Changed GetMaxLightningCount() function into a global constant, to prevent lightning array problems.
  • Slightly changed the beam's color to have a bit of an orange hue.
25.8.2018:
  • Removed hashtable usage
  • Added more configuration functions
  • Changed the lightning model to Magic Leash lightning effect
  • Changed scope into library, now requires TimerUtils and a UnitIndexer
  • Removed some leaks from the test map
  • Removed Railgun_ prefixes

  • Adjust unit group in Fire function, now it doesn't create and destroy multiple groups, but only creates one and destroys it.
  • Fixed test map's Attribute Bonus ability.

  • Forgot to edit comments.. there they are.

23.8.2018:
  • Placed everything under a scope
  • Moved most of globals to be functions
  • Replaced TriggerAddAction with TriggerAddCondition
  • Made a configurable hashtable section for those who may want to incorporate this into 1 hashtable
  • Cleaned up some leftover not-nulled variables

19.7.2018:
  • Removed vJASS code,
  • Added/Changed/Removed some configuration
  • Removed excessive over-use of GetLocationZ
  • Changed the code, now spawns only 5 beams that are gradually getting longer with distance

27.5.2018:
  • Added some newlines in the code for more readability
  • Changed most, if not, all the text to the JPAG standard
  • Added an option for the end-user to either use LocationZ or not
  • nulled some variables
  • Added a spellshield effect to the caster when firing the railgun

2.3.2018:
  • Indented some words
  • Added some blank lines between functions
  • Replaced the eye-bleed-inducing structures with arrays
  • Privatized globals and removed their prefixes
Previews
Contents

Railgun (Map)

Railgun (Map)

Reviews
MyPad
Nitpicks: Constants which are configurable could be moved to functions instead. Adding some more effects to the railgun when firing would be appreciated. Notes: There is a lot to be desired from readability. Try to observe the JPAG standard...
Bribe
Instead of prefixing everything with Railgun_, use the "private" keyword. This doesn't need a hashtable. A unit indexer, which many maps already have, would serve better (and faster). Work towards implementing more of these kinds of changes (such as...
Cokemonkey11
Past feedback has been addressed, code is configurable and well-commented. The spell is relatively simple but not one I see often. Approved.

Nitpicks:

  • Constants which are configurable could be moved to functions instead.
  • Adding some more effects to the railgun when firing would be appreciated.
Notes:

  • There is a lot to be desired from readability.
  • Try to observe the JPAG standard when submitting resources such as these.
    • Constants should be in UPPER_CASE
    • Non-constants, locals, variables should be camelCase
    • Functions should be LikeThis.
  • Null your variables that are handles or extensions thereof.
Status:
  • [color]Awaiting Update.[/color]


Notes:

  • As the code is written in such a way that it would not normally compile on regular World Editor (before patch 1.30), I would suggest enclosing this in a scope.
  • Caps-Locked variables are constants in normal JPAG convention, but in the spell, these are not constants.
  • Functions can be preceded with a private prefix to save you some time in typing.
  • In function Railgun_DamageVictims, a float literal was spotted on the declaration of a local real variable damage.
    local real damage = 0.16*GetUnitState(target,UNIT_STATE_MAX_LIFE)
  • In function Railgun_DestructableBlackList, the statement can be shortened from the following:
    JASS:
    local unit caster           = GetTriggerUnit()
    local integer destructType  = GetDestructableTypeId(GetFilterDestructable())
       
    //if the destructable is alive and is not a pathblocker of any kind.
    if GetDestructableLife(GetFilterDestructable()) <= 0 then 
        return false
    endif
    if destructType == 'YTlb' then
        call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
        return false
    endif
    if destructType == 'YTab' then
        call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
        return false
    endif
    if destructType == 'YTpb' then
        call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
        return false
    endif
    if destructType == 'YTfb' then 
        call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
        return false
    endif
    call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
    return true

    to

    JASS:
    local unit caster           = GetTriggerUnit()
    local integer destructType  = GetDestructableTypeId(GetFilterDestructable())
       
    //if the destructable is alive and is not a pathblocker of any kind.
    if GetDestructableLife(GetFilterDestructable()) <= 0 then 
        return false
    endif
    
    call SaveBoolean(RAILGUN_TABLE,GetHandleId(caster),StringHash("Railgun_Rangelimit"),true)
    return (destructType == 'YTlb') or (destructType == 'YTab') or (destructType == 'YTpb') or (destructType == 'YTfb')
  • In function Railgun_UnitBlackList, you can delete the local variable declaration. (It is not particularly useful).
  • In functions Railgun_Cast, Railgun_Stop, and Railgun_Fire, you can merge the checking of the correct ability being cast into said functions.
    You can also use TriggerAddCondition instead of TriggerAddAction if only for an incremental performance gain.
  • In function Railgun_Cast, within the first loop, the variable visualizer is assigned to a newly created unit owned by the Passive player. You can store the passive player into a variable, since its' value is constant.

    The local group variable visualization is never nulled.

  • In function Railgun_Stop, the locally declared group g is not nulled.

    As much as FirstOfGroup is preferred over ForGroup here, the possibility of ghost units being included may prematurely end the loop.

  • In function Railgun_FireCleanup, using a FirstOfGroup over a ForGroup is preferable, since the dummy units aren't likely to be removed by anything else.
  • In function Railgun_Fire, on the second loop, using a FirstOfGroup over a ForGroup is preferable, as the type of iteration is destructive.

Nitpicks:

  • Using string hashes may be fine, but beware of using them too often, as they are likely to produce key collisions.
  • You might want to add a warning to all who will use this, that this uses up one hashtable out of 255-256.
  • The spell could use some constant functions, which store information about a particular aspect of the spell.

Status:

  • Awaiting Update
 
Last edited:
Instead of prefixing everything with Railgun_, use the "private" keyword.

This doesn't need a hashtable. A unit indexer, which many maps already have, would serve better (and faster).

Work towards implementing more of these kinds of changes (such as the ones from MyPad), and I'll return here to give it another look once you've done so. I want to see how many things you spot on your own, first, before I point them out.

This resource looks cool. Your code will get more refined. Lots of room for growth.
 
Is it possible to use a imported asset for the
private string VISUAL_HIT_MODEL = "Objects\\Spawnmodels\\NightElf\\NECancelDeath\\NECancelDeath.mdl"
I tried setting it as war3mapImported\ShrapnelShards.mdx (I'm using the shrapnel shards model) but it gave me an error and said "Invalid Escape character sequence".
Am I doing smth wrong or is it just impossible to set it to an imported model?
 
Back
Top