do
local cnf = {
-- The update period of the system
PERIOD = 1./40.,
-- The max amount of Missiles processed in a PERIOD
-- You can play around with both these values to find
-- your sweet spot. If equal to 0, the system will
-- process all missiles at once every period.
SWEET_SPOT = 150,
-- the avarage collision size compensation when detecting
-- collisions
COLLISION_SIZE = 128.,
-- item size used in z collision
ITEM_SIZE = 16.,
-- set this to true if you want the system to orient
-- the missile roll. Roll can look really fishy for
-- some users and it was never possible to be set
-- until recent patches, so use it if you want.
ROLL = false,
-- Needed, dont touch. Seriously, dont touch!
LOC = Location(0., 0.),
RECT = Rect(0., 0., 0., 0.),
hitTable = {}
}
local function GetLocZ(x, y)
MoveLocation(cnf.LOC, x, y)
return GetLocationZ(cnf.LOC)
end
-- Coordinates
Coordinates = setmetatable({}, {})
local mt = getmetatable(Coordinates)
mt.__index = mt
function mt:math(a, b)
local dx
local dy
while true do
dx = b.x - a.x
dy = b.y - a.y
dx = dx*dx + dy*dy
dy = SquareRoot(dx)
if dx ~= 0. and dy ~= 0. then break end
b.x = b.x + .01
b.z = b.z - GetLocZ(b.x -.01, b.y) + GetLocZ(b.x, b.y)
end
a.square = dx
a.distance = dy
a.angle = Atan2(b.y - a.y, b.x - a.x)
a.slope = (b.z - a.z)/dy
a.alpha = Atan(a.slope)
-- Set b.
if b.ref == a then
b.angle = a.angle + bj_PI
b.distance = dy
b.slope = -a.slope
b.alpha = -a.alpha
b.square = dx
end
end
function mt:link(a, b)
a.ref = b
b.ref = a
self:math(a, b)
end
function mt:move(toX, toY, toZ)
self.x = toX
self.y = toY
self.z = toZ + GetLocZ(toX, toY)
if self.ref ~= self then
math(self, self.ref)
end
end
function mt:create(x, y, z)
local c = {}
setmetatable(c, mt)
c.ref = c
c:move(x, y, z)
return c
end
-- Missiles static members
local Static = {
t = CreateTimer(),
hitGroup = CreateGroup(),
didx = -1,
last = 0,
dilation = 0.,
temp = nil,
missiles = {}
}
Static.__index = Static
Static.onDest = function()
local d = GetEnumDestructable()
local dz
local tz
if cnf.hitTable[Static.temp][GetHandleId(d)] == nil then
if Static.temp.collideZ then
dz = GetLocZ(GetWidgetX(d), GetWidgetY(d)) - GetLocZ(Static.temp.x, Static.temp.y)
tz = GetDestructableOccluderHeight(d)
if dz + tz >= Static.temp.z - Static.temp.collision and dz <= Static.temp.z + Static.temp.collision then
cnf.hitTable[Static.temp][GetHandleId(d)] = true
if Static.temp.allocated and Static.temp.onDestructable(d) then
d = nil
Static.temp:terminate()
return
end
end
else
cnf.hitTable[Static.temp][GetHandleId(d)] = true
if Static.temp.allocated and Static.temp.onDestructable(d) then
d = nil
Static.temp:terminate()
return
end
end
end
d = nil
end
Static.onItem = function()
local i = GetEnumDestructable()
local dz
if cnf.hitTable[Static.temp][GetHandleId(i)] == nil then
if collideZ then
dz = GetLocZ(GetItemX(Static.temp.i), GetItemY(Static.temp.i)) - GetLocZ(Static.temp.x, Static.temp.y)
if dz + cnf.ITEM_SIZE >= Static.temp.z - Static.temp.collision and dz <= Static.temp.z + Static.temp.collision then
cnf.hitTable[Static.temp][GetHandleId(i)] = true
if Static.temp.allocated and Static.temp.onItem(i) then
i = nil
Static.temp:terminate()
return
end
end
else
cnf.hitTable[Static.temp][GetHandleId(i)] = true
if Static.temp.allocated and Static.temp.onItem(i) then
i = nil
Static.temp:terminate()
return
end
end
end
i = nil
end
---------------------------- Missiles movement ---------------------------
Static.move = function()
local j = 0
local i = 0
local k = 0
local u = nil
local a = 0.
local d = 0.
local s = 0.
local h = 0.
local c = 0.
local dx = 0.
local dy = 0.
local vel = 0.
local yaw = 0.
local pitch = 0.
local locZ = 0.
local missile = nil
local o = nil
local this = nil
-- SWEET_SPOT used to enable or disable
-- relativistic processing
if cnf.SWEET_SPOT > 0 then
i = Static.last
else
i = 0
end
while true do
if (j >= cnf.SWEET_SPOT and cnf.SWEET_SPOT > 0) or j > Static.didx then break end
this = Static.missiles[i]
Static.temp = this
if this.allocated then
o = this.origin
h = this.height
c = this.open
d = o.distance
locZ = GetLocZ(this.x, this.y)
--onPeriod Event
if this.onPeriod ~= nil then
if allocated and this.onPeriod() then
this:terminate()
end
end
-- onHit Event
if this.onHit ~= nil then
if this.allocated and this.collision > 0 then
GroupEnumUnitsInRange(Static.hitGroup, this.x, this.y, this.collision + cnf.COLLISION_SIZE, nil)
while true do
u = FirstOfGroup(Static.hitGroup)
if u == nil then break end
if cnf.hitTable[this][GetHandleId(u)] == nil then
if IsUnitInRangeXY(u, this.x, this.y, this.collision) then
if this.collideZ then
dx = GetLocZ(GetUnitX(u), GetUnitY(u)) + GetUnitFlyHeight(u) - this.locZ
dy = BlzGetUnitCollisionSize(u)
if dx + dy >= this.z - this.collision and dx <= this.z + this.collision then
cnf.hitTable[this][GetHandleId(u)] = true
if this.allocated and this.onHit(u) then
this:terminate()
break
end
end
else
cnf.hitTable[this][GetHandleId(u)] = true
if this.allocated and this.onHit(u) then
this:terminate()
break
end
end
end
end
GroupRemoveUnit(Static.hitGroup, u)
end
end
end
-- onMissile Event
if this.onMissile ~= nil then
if allocated and collision > 0 then
k = 0
while true do
if k > Static.didx then break end
missile = Static.missiles[k]
if missile ~= this then
if cnf.hitTable[this][missile] == nil then
dx = missile.x - this.x
dy = missile.y - this.y
if SquareRoot(dx*dx + dy*dy) <= this.collision then
cnf.hitTable[this][missile] = true
if this.allocated and this.onMissile(missile) then
this:terminate()
break
end
end
end
end
k = k + 1
end
end
end
-- onDestructable Event
if this.onDestructable ~= nil then
if this.allocated and this.collision > 0 then
dx = this.collision
SetRect(cnf.RECT, this.x - dx, this.y - dx, this.x + dx, this.y + dx)
EnumDestructablesInRect(cnf.RECT, nil, Static.onDest)
end
end
-- onItem Event
if this.onItem ~= nil then
if allocated and collision > 0 then
dx = collision
SetRect(cnf.RECT, x - dx, y - dx, x + dx, y + dx)
EnumItemsInRect(cnf.RECT, nil, Static.onItem)
end
end
-- Homing or not
u = target
if u ~= nil and GetUnitTypeId(u) ~= 0 then
this.impact.move(GetUnitX(u), GetUnitY(u), GetUnitFlyHeight(u) + this.toZ)
dx = this.impact.x - this.prevX
dy = this.impact.y - this.prevY
a = Atan2(dy, dx)
this.travel = o.distance - SquareRoot(dx*dx + dy*dy)
else
a = o.angle
this.target = nil
end
-- turn rate
if this.turn ~= 0 and not (Cos(this.cA-a) >= Cos(this.turn)) then
if Sin(a-this.cA) >= 0 then
this.cA = this.cA + this.turn
else
this.cA = this.cA - this.turn
end
else
this.cA = a
end
vel = this.veloc*Static.dilation
yaw = this.cA
pitch = o.alpha
this.x = this.prevX + vel*Cos(yaw)
this.y = this.prevY + vel*Sin(yaw)
s = this.travel + vel
this.prevX = this.x
this.prevY = this.y
this.prevZ = this.z
this.veloc = this.veloc + this.acceleration
this.travel = s
-- arc calculation
if h ~= 0 or o.slope ~= 0 then
this.z = 4*h*s*(d-s)/(d*d) + o.slope*s + o.z
pitch = pitch - Atan(((4*h)*(2*s - d))/(d*d))
end
-- curve calculation
if c ~= 0 then
dx = 4*c*s*(d-s)/(d*d)
a = yaw + bj_PI/2
this.x = this.x + dx*Cos(a)
this.y = this.y + dx*Sin(a)
yaw = yaw + Atan(-((4*c)*(2*s - d))/(d*d))
end
-- onTerrain event
if this.onTerrain ~= nil then
if GetLocZ(this.x, this.y) > this.z then
if this.allocated and this.onTerrain() then
this:terminate()
end
end
end
if s >= d - 0.0001 then
-- onFinish event
if this.onFinish ~= nil then
if this.allocated and this.onFinish() then
this:terminate()
else
-- deflected onFinish
if this.travel > 0 then
this:terminate()
end
end
else
this:terminate()
end
else
if cnf.ROLL then
this.effect:orient(yaw, -pitch, Atan2(c, h))
else
this.effect:orient(yaw, -pitch, 0)
end
end
if not this.effect:move(this.x, this.y, this.z) then
-- onBoundaries event
if this.onBoundaries ~= nil then
if this.allocated and this.onBoundaries() then
this:terminate()
end
end
end
else
i = this:remove(i)
j = j - 1
end
i = i + 1
j = j + 1
if i > Static.didx and cnf.SWEET_SPOT > 0 then
i = 0
end
end
Static.last = i
u = nil
end
-- Missiles
Missiles = setmetatable({}, {})
local mt = getmetatable(Missiles)
mt.__index = mt
-------------------------- Model of the missile --------------------------
function mt:setModel(fx)
DestroyEffect(self.effect.effect)
self.effect.path = fx
self.effect.effect = AddSpecialEffect(fx, self.origin.x, self.origin.y)
BlzSetSpecialEffectZ(self.effect.effect, self.origin.z)
end
----------------------------- Curved movement ----------------------------
function mt:setCurve(value)
self.open = Tan(value*bj_DEGTORAD)*self.origin.distance
end
----------------------------- Arced Movement -----------------------------
function mt:setArc(value)
self.height = Tan(value*bj_DEGTORAD)*self.origin.distance/4
end
------------------------------ Effect scale ------------------------------
function mt:setScale(value)
self.effect.size = value
self.effect:scale(self.effect.effect, value)
end
------------------------------ Missile Speed -----------------------------
function mt:setSpeed(value)
self.veloc = value*cnf.PERIOD
end
------------------------------- Flight Time ------------------------------
function mt:setDuration(value)
self.veloc = RMaxBJ(0.00000001, (self.origin.distance - self.travel)*cnf.PERIOD/RMaxBJ(0.00000001, value))
end
---------------------------- Bound and Deflect ---------------------------
function mt:bounce()
local locZ = GetLocZ(self.x, self.y)
-- This is here just to avoid an infinite loop
-- with a deflect being called within an onTerrain
-- event
if z < locZ then
self.z = locZ
self.impact:move(self.impact.x, self.impact.y, self.origin.z - GetLocZ(self.impact.x, self.impact.y))
end
self.origin:move(self.x, self.y, self.origin.z - GetLocZ(self.origin.x, self.origin.y))
self.travel = 0
end
function mt:deflect(tx, ty)
if self.target ~= nil then
self.target = nil
end
self.impact:move(self.tx, self.ty, self.impact.z - GetLocZ(self.impact.x, self.impact.y))
self:bounce()
end
function mt:deflectZ(tx, ty, tz)
self.impact:move(self.impact.x, self.impact.y, tz)
self:deflect(tx, ty)
end
---------------------------- Flush hit targets ---------------------------
function mt:flushAll()
cnf.hitTable[self] = nil
end
function mt:flush(w)
if w ~= nil then
cnf.hitTable[self][GetHandleId(w)] = nil
end
end
function mt:hitted(w)
return cnf.hitTable[self][GetHandleId(w)]
end
----------------------- Missile attachment methods -----------------------
-- dx, dy, and dz are offsets from the main missile coordinates
--method attach takes string model, real dx, real dy, real dz, real scale returns effect
-- return effect.attach(model, dx, dy, dz, scale)
--endmethod
--
--method detach takes effect attachment returns nothing
-- if attachment != null then
-- call effect.detach(attachment)
-- endif
--endmethod
------------------------------ Reset members -----------------------------
function mt:reset()
self.launched = false
self.collideZ = false
self.source = nil
self.target = nil
self.owner = nil
self.open = 0.
self.height = 0.
self.veloc = 0.
self.acceleration = 0.
self.collision = 0.
self.damage = 0.
self.travel = 0.
self.turn = 0.
self.data = 0.
self.onHit = nil
self.onMissile = nil
self.onDestructable = nil
self.onItem = nil
self.onTerrain = nil
self.onFinish = nil
self.onBoundaries = nil
self.onRemove = nil
end
-------------------------------- Terminate -------------------------------
function mt:terminate()
if self.allocated then
self.allocated = false
-- onRemove event
if self.onRemove ~= nil then
self.onRemove()
end
cnf.hitTable[self] = nil
end
end
-------------------------- Destroys the missile --------------------------
function mt:remove(i)
self:terminate()
--self.origin:destroy()
--self.impact:destroy()
self.effect:destroy()
self:reset()
Static.missiles[i] = Static.missiles[Static.didx]
Static.didx = Static.didx - 1
-- Compensation for time dilation
if Static.didx + 1 > cnf.SWEET_SPOT and cnf.SWEET_SPOT > 0 then
Static.dilation = (Static.didx + 1)/cnf.SWEET_SPOT
else
Static.dilation = 1
end
if Static.didx == -1 then
PauseTimer(Static.t)
end
self = nil
--self:deallocate()
return i - 1
end
--------------------------- Launch the Missile ---------------------------
function mt:launch()
if not self.launched and self.allocated then
self.launched = true
Static.didx = Static.didx + 1
Static.missiles[Static.didx] = self
-- Compensation for time dilation
if Static.didx + 1 > cnf.SWEET_SPOT and cnf.SWEET_SPOT > 0 then
Static.dilation = (Static.didx + 1)/cnf.SWEET_SPOT
else
Static.dilation = 1.
end
if Static.didx == 0 then
TimerStart(Static.t, cnf.PERIOD, true, Static.move)
end
end
end
--------------------------- Main Creator method --------------------------
function mt:create(x, y, z, toX, toY, toZ)
local m = {}
setmetatable(m, mt)
cnf.hitTable[m] = {}
m:reset()
m.origin = Coordinates:create(x, y, z)
m.impact = Coordinates:create(toX, toY, toZ)
m.allocated = true
m.x = x
m.y = y
m.z = z
m.prevX = x
m.prevY = y
m.prevZ = z
m.toZ = toZ
Coordinates:link(m.origin, m.impact)
m.cA = m.origin.angle
m.effect = MissileEffect:create(x, y, z)
return m
end
end