- Joined
- Jul 4, 2016
- Messages
- 636
Going to test it out now that it is publicly available.
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
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
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