library Projectiles requires Vectors optional TimerUtils
globals
//**********************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Many default constants.
//*
//*
private constant integer FLY_ABIL_ID = 'Amrf' //* This is medihv's crow-form ability.
//*
public constant real COLLISION_DEFAULT = 50 //* This is the range at which units/projectiles will "collide".
public constant real COLLISION_MAX = 180 //* This is the maximum value for projectile collision. If 'COLLISION_DEFAULT'
//* is higher then this it will not over-ride it.
//*
public constant boolean EXPIRATION_DEFAULT = true //* The projectile's tendency to die once it has "finished". Setting this to false
//* should only be done if you are managing the projectiles yourself.
//*
private constant boolean hitDead = false //* Projectiles will "strike" dead units.
private constant boolean hitEnemy = true //* Projectiles will "strike" enemy units.
private constant boolean hitAllied = false //* Projectiles will "strike" allied units.
private constant boolean hitStructure = false //* Projectiles will "strike" structures.
//*
//*
//*****************************************
endglobals
interface projectileinterface
//*************
//* Projectile Interface
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* The following functions can be re-declared in child-structs and gain their logical functionality. For example, declaring
//* 'onUnitCollision' will allow you to customize the actions executed when the projectile comes into contact with a unit.
//*
//*
method onFinish takes nothing returns nothing defaults nothing //* Once the projectile reaches the end of its path (unless otherwise specified).
method onBirth takes nothing returns nothing defaults nothing //* When the projectile starts its projection path.
method onUnitCollision takes unit u returns nothing defaults nothing //* When the projectile collides with a unit (u = collide with -unit-).
method onCollision takes projectile p returns nothing defaults nothing //* When the projectile collides with another projectile (p = other projectile).
method onGround takes nothing returns nothing defaults nothing //* Projectile has struck the ground.
//*
//*
endinterface
struct projectile extends projectileinterface
//***************************
//* Projectiles
//* ¯¯¯¯¯¯¯¯¯¯¯
//* Data-structure that supports the creation/launch (as well as other functions) of in-game projectiles. The projectiles
//* are meant to mimic the basic ones as close as possible.
//*
//*
// Primary handle-base for the projectile.
readonly unit toUnit = null //* This unit is "projected" as if it were a projectile.
readonly real speed
readonly real arc
real collision = COLLISION_DEFAULT
real timescale = 1.00 //* Time-scale is at 100% by default.
private real p_scale = 1.00 //* Size-scale is at 100% by default.
method operator scale= takes real s returns nothing
set .p_scale=s
call SetUnitScale(.toUnit, s, s, s)
endmethod
method operator scale takes nothing returns real
return .p_scale
endmethod
// Active feature data
boolean activeUnitCollision = true //* The projectile can collide with units.
boolean activeCollision = false //* The projectile can collide with other projectiles.
boolean activePitchRotation = true //* The projectile will "lean" in the direction that it is moving.
private static location p_sampleLoc = Location(0, 0) //* Location used often for 'MoveLocatin' and 'GetLocationZ'
private static group p_sampleGroup = CreateGroup() //* Group used to enumerate nearby units.
private static thistype p_sampleProj //* Used to store 'this' when doing a group enum.
// These vectors store information including projectile motion.
private s_vector3d p_currentPos
private s_vector3d p_currentVelocity
private s_vector3d p_currentAcceleration
private s_vector3d p_currentTargetPos //* When the target-position vector is changed during flight, it will activate the projectile's
//* "homing" capabilities, which will eliminate 'arc' and move directly towards the target.
private real p_timeRemaining
// --------
// The following method operators allow the user more access to the projectile's vectors, without being able to
// do anything "illegal".
//
// Returns a COPY of the position vector.
method operator position takes nothing returns s_vector3d
return .p_currentPos.copy()
endmethod
// Returns a COPY of the velocity vector.
method operator velocity takes nothing returns s_vector3d
return .p_currentVelocity.copy()
endmethod
// Returns a COPY of the acceleration vector.
method operator acceleration takes nothing returns s_vector3d
return .p_currentAcceleration.copy()
endmethod
// Returns a COPY of the target-position vector.
method operator targetposition takes nothing returns s_vector3d
return .p_currentTargetPos.copy()
endmethod
//
// Destroy removes projectile from the stack, but it does not kill the unit.
method onDestroy takes nothing returns nothing
set thistype.p_stackTrack = thistype.p_stackTrack - 1
set thistype.p_stack[this.p_index] = thistype.p_stack[thistype.p_stackTrack]
set thistype.p_stack[this.p_index].p_index = this.p_index
if (thistype.p_stackTrack==0) then
call PauseTimer(thistype.p_loop) //* Pause the timer if it is no longer needed.
endif
// Don't forget to destroy the vectors!
call .p_currentPos.destroy()
call .p_currentTargetPos.destroy()
call .p_currentVelocity.destroy()
call .p_currentAcceleration.destroy()
endmethod
// We don't want the projectile struct to leak so we need to remove it once its trajectory has completed.
method onFinish takes nothing returns nothing
call this.destroy()
endmethod
// In order to keep track of the projectile within a stack of projectiles, it must have necessary indexing mechanisms. When
// a projectile is created it is added to this stack, which is looped through on each iteration of the timer.
private integer p_index //* Keeps track of 'this' index so that the stack can be maintained.
private static thistype array p_stack
private static integer p_stackTrack = 0 //* There are no projectiles in the stack to begin with.
// In order to update the stack, we must use a timer. If the 'TimerUtils' library is available then it will use 'NewTimer' instead of 'CreateTimer'.
private static constant real p_loopPeriod = 0.03 //* Rate that the timer will update the stack. Increasing it will decrease the performance.
static if (LIBRARY_TimerUtils) then
private static timer p_loop = NewTimer() //* Uses TimerUtils. The timer is never destroyed so its not that important, I just wanted to
else //* use the 'optional' keyword and try out static if statements.
private static timer p_loop = CreateTimer() //* This does not.
endif
boolean toDestroy = EXPIRATION_DEFAULT //* If this is 'false' then the projectile will not run .onFinish() and its remaining time
//* will not decrease.
static method onLoopUnitEnum takes nothing returns boolean
local thistype this = .p_sampleProj
local unit filt = GetFilterUnit()
local integer c = 0
local real x
local real y
local real z
set x = GetUnitX(filt)-this.p_currentPos.x
set y = GetUnitY(filt)-this.p_currentPos.y
call MoveLocation(.p_sampleLoc, GetUnitX(filt), GetUnitY(filt))
set z = GetLocationZ(.p_sampleLoc)+GetUnitFlyHeight(filt) - this.p_currentPos.z
if (SquareRoot(x*x + y*y + z*z) <= this.collision) and (filt!=this.toUnit) then
call this.onUnitCollision(filt)
endif
set filt = null
return false
endmethod
// Updates stack .p_stack on a periodic timer.
static method onLoopStatic takes nothing returns nothing
local integer i = .p_stackTrack - 1
local integer j
local thistype this
local s_vector3d vec1
local s_vector3d vec2
local real x //* These are not necessary, but they allocate some of the processor's
local real y //* work-load onto the memory.
local real z
loop
exitwhen (i < 0)
set this = .p_stack[i]
if (this != 0) then
set vec1 = this.p_currentVelocity
set vec2 = this.p_currentAcceleration
// Scale the added acceleration based on the time-scale of the projectile.
set vec1.x = vec1.x+(vec2.x*this.timescale*this.timescale)
set vec1.y = vec1.y+(vec2.y*this.timescale*this.timescale)
set vec1.z = vec1.z+(vec2.z*this.timescale*this.timescale)
set vec2 = this.p_currentPos
// Scale the added velocity (to position) based on time-scale.
set vec2.x = vec2.x+(vec1.x*this.timescale)
set vec2.y = vec2.y+(vec1.y*this.timescale)
set vec2.z = vec2.z+(vec1.z*this.timescale)
// Update the projectile.
set x = vec2.x
set y = vec2.y
call SetUnitX(this.toUnit, x)
call SetUnitY(this.toUnit, y)
call MoveLocation(.p_sampleLoc, x, y)
call SetUnitFlyHeight(this.toUnit, vec2.z-GetLocationZ(.p_sampleLoc), 0)
// Update the projectile's pitch-rotation, if applicable.
if (this.activePitchRotation and vec1.magnitude()!=0.0) then
set x = vec1.x
set y = vec1.y
set z = vec1.z
call SetUnitFacingTimed(this.toUnit, Atan2(y, x)*bj_RADTODEG, 0)
call SetUnitAnimationByIndex(this.toUnit, R2I(bj_RADTODEG*Atan2(z, SquareRoot(x*x + y*y)+0.5)+90))
endif
// Since 'doSync' was just called, .p_sampeLoc will still have the correct coordinates.
if (this.p_currentPos.z <= GetLocationZ(.p_sampleLoc)) then
// Indicates that the projectile has struck the ground.
call this.onGround()
endif
// Only execute the projectile-collision if it is activated. Currently this system does not
// support projectile-projectile collision.
if (this.activeCollision) then
// >>>>
endif
if (this.toDestroy) then
set this.p_timeRemaining = this.p_timeRemaining - .p_loopPeriod
if (this.p_timeRemaining <= 0.00) then
// Indicates the projectile's trajectory is complete.
call this.onFinish()
endif
endif
// Only execute unit-collision segment if the projectile unit-collision is activated.
if (this.activeUnitCollision) then
// Enumerate nearby units so that they can be "detected". Store 'this' in global variable for referencing in enum response.
set .p_sampleProj = this
call GroupEnumUnitsInRange(.p_sampleGroup, vec2.x, vec2.y, COLLISION_MAX, Filter(function thistype.onLoopUnitEnum))
// The enumeration returns false, so the group will always remain empty.
endif
endif
set i = i - 1
endloop
endmethod
// Launches the projectile with provided input. If the projectile cannot be created with specified input then this
// method will return false.
method doLaunch takes s_vector3d start, s_vector3d target, real speed, real pitchArc returns boolean
local real d
local real a
if (start==0) or (target==0) then
return false
endif
// Initialize vectors and specific info.
set .p_currentPos = start.copy()
set .p_currentTargetPos = target.copy()
set .speed = speed //* Speed/arc are never used internally, but they are there for user reference in any
set .arc = pitchArc //* case.
// The timer must be started if this is the first projectile in-stack.
if (thistype.p_stackTrack==0) then
call TimerStart(thistype.p_loop, thistype.p_loopPeriod, true, function thistype.onLoopStatic)
endif
// Add the projectile to the stack.
set .p_index = thistype.p_stackTrack
set thistype.p_stack[thistype.p_stackTrack] = this
set thistype.p_stackTrack = thistype.p_stackTrack + 1
//
set d = SquareRoot((start.x-target.x)*(start.x-target.x) + (start.y-target.y)*(start.y-target.y)) //* This is a 2D distance.
set a = Atan2(target.y-start.y, target.x-start.x)
if (d==0.0) or (speed==0) then
set .p_currentPos.x = .p_currentTargetPos.x
set .p_currentPos.y = .p_currentTargetPos.y
set .p_currentPos.z = .p_currentTargetPos.z
set .p_timeRemaining = 0.0
// This indicates the projectile will be instant.
set .p_currentAcceleration = s_vector3d.createEx(0, 0, 0)
set .p_currentVelocity = s_vector3d.createEx(0, 0, 0)
// The projectile should be destroyed immediately (ignoring the inaccuracy of the timer).
else
set .p_timeRemaining = d/speed
set .p_currentAcceleration = s_vector3d.createEx(0, 0, -8*pitchArc*speed*speed/d)
set .p_currentVelocity = s_vector3d.createEx(speed*Cos(a), speed*Sin(a), -.p_currentAcceleration.z * (d/speed)/2 + (target.z-start.z)/(d/speed))
endif
// Scale projectile vectors
set .p_currentAcceleration.x=.p_currentAcceleration.x*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentAcceleration.y=.p_currentAcceleration.y*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentAcceleration.z=.p_currentAcceleration.z*thistype.p_loopPeriod*thistype.p_loopPeriod
set .p_currentVelocity.x=.p_currentVelocity.x*thistype.p_loopPeriod
set .p_currentVelocity.y=.p_currentVelocity.y*thistype.p_loopPeriod
set .p_currentVelocity.z=.p_currentVelocity.z*thistype.p_loopPeriod
// Sync projectile and call .onBirth user-method. If a projectile is "instant" it will not be destroyed until the first
// timer-loop. This is a maximum inaccuracy of the timer's loop timeout.
call SetUnitX(.toUnit, .p_currentPos.x)
call SetUnitY(.toUnit, .p_currentPos.y)
call MoveLocation(thistype.p_sampleLoc, .p_currentPos.x, .p_currentPos.y)
call SetUnitFlyHeight(.toUnit, .p_currentPos.z-GetLocationZ(thistype.p_sampleLoc), 0)
call .onBirth()
return true
endmethod
// Creates a projectile based on a unit.
static method create takes unit u returns thistype
local thistype proj = thistype.allocate()
set proj.toUnit = u
call UnitAddAbility(u, FLY_ABIL_ID)
call UnitRemoveAbility(u, FLY_ABIL_ID)
return proj
endmethod
endstruct
endlibrary