- Joined
- Mar 29, 2016
- Messages
- 688
Missile
For Patches 1.31+
For Patches 1.31+
New Version Summary:
This update is a continuation to BPower's Missile library. Hence, it has similar API to that available in version 2.5.1, except for the depreciated features, which I completely removed in this new version. I also took freedom to change/rename the not-so-commonly used public functions. I don't have the space to list all these changes so whatever you do not see in the API documentation that is previously there means it is removed. But you can be assured that about 95 percent of the Missile struct members remained unchanged.
Another important change to mention is the incorporation of 'roll' in the missile orientation. Previously, only yaw and pitch are considered. Now it is able to do this as you can see in the preview.
Notice that the roll is now dependent on the arc and curve of the Missile.
Missile.j
JASS:
library Missile /* version 3.2.0 (Update for patches 1.31+)
*************************************************************************************
*
* Resource link:
* https://www.hiveworkshop.com/threads/missile.325956/
*
* Legacy Resource Link (for patches 1.30.x and below):
* https://www.hiveworkshop.com/threads/missile.265370/
*
*************************************************************************************
*
* Original Resource by BPower, updated by AGD
*
* Credits to Dirac, emjlr3, AceHart, Bribe, Wietlol, Nestharus,
* Maghteridon96, Vexorian, Zwiebelchen, and Chopinski
*
*************************************************************************************
*
* Creating custom projectiles in Warcraft III.
*
* Philosophy:
* I want that feature --> Compiler writes that code into your map script.
* I don't want that --> Compiler ignores that code completely.
*
* Important:
* Take yourself 2 minutes time to setup Missile correctly.
* Otherwise I can't guarantee, that Missile works the way you want.
* Once the setup is done, you can check out some examples and Missile will be easy
* to use for everyone. I promise it.
*
* Do the setup at:
*
* 1.) Import instruction
* 2.) Global configuration
* 3.) Function configuration
*
*************************************************************************************
*
* */ requires /*
*
* */ Table /* https://www.hiveworkshop.com/threads/188084/
* */ SpecialEffect /* https://www.hiveworkshop.com/threads/325954/ | Should use atleast v1.1.0
* */ LinkedList /* https://www.hiveworkshop.com/threads/325635/
*
*************************************************************************************
*
* */ optional Alloc /* https://www.hiveworkshop.com/threads/324937/
* */ optional ErrorMessage /* https://github.com/nestharus/JASS/blob/master/jass/Systems/ErrorMessage/main.j
*
************************************************************************************
*
* 1. Import instruction
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
* • Import the required libraries.
* • Copy Missile into to your map.
*
* 2. Global configuration
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*/
globals
/**
* Missiles are moved periodically. An interval of 1./32. is recommended.
* • Too short timeout intervals may cause performance issues.
* • Too large timeout intervals may look fishy.
*/
public constant real TIMER_TIMEOUT = 1./32.
/**
* The maximum collision size used in your map. If unsure use 197. ( Town hall collision )
* • Applies for all types of widgets.
* • A precise value can improve Missile's performance,
* since smaller values enumerate less widgtes per loop per missile.
*/
public constant real MAXIMUM_COLLISION_SIZE = 197.
/**
* Collision types for missiles. ( Documentation only )
* Missile decides internally each loop which type of collision is required.
* • Uses circular collision dectection for speed < collision. ( Good accuracy, best performance )
* • Uses rectangle collision for speed >= collision. ( Best accuracy, normal performance )
*/
public constant integer COLLISION_TYPE_CIRCLE = 0
public constant integer COLLISION_TYPE_RECTANGLE = 1
/**
* Determine when rectangle collision is required to detect nearby widgets.
* • The smaller the factor the earlier rectangle collision is used. ( by default 1. )
* • Formula: speed >= collision*Missile_COLLISION_ACCURACY_FACTOR
*/
public constant real COLLISION_ACCURACY_FACTOR = 1.
/*
Optional toogles:
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Set booleans listed below to "true" and the compiler will write
the feature into your map. Otherwise this code is completly ignored.
• Yay, I want that --> "true"
• Naah that's useless for me --> "false"
**
* USE_COLLISION_Z_FILTER enables z axis checks for widget collision. ( Adds minimal overhead )
* Use it when you need:
* • Missiles flying over or under widgets.
* • Determine between flying and walking units.
*/
public constant boolean USE_COLLISION_Z_FILTER = true
/**
* DELAYED_MISSILE_DEATH_ANIMATION_TIME is the delay in seconds
* to accomodate for the sfx death duration of destroyed Missiles
*/
private constant real DELAYED_MISSILE_DEATH_ANIMATION_TIME = 2.16
endglobals
/*
* 3. Function configuration
* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*/
/**
* GetUnitHeight(unit) returns a fictional value for z - axis collision.
* You have two options:
* • One constant value shared over all units.
* • Dynamic values based on handle id, type id, unit user data, scaling or other parameters.
*/
public function GetUnitHeight takes unit whichUnit returns real
return 108.// Other example: return LoadReal(hash, GetHandleId(whichUnit), KEY_UNIT_HEIGHT)
endfunction
/**
* Same as GetUnitHeight, but for destructables.
* Using occluder height is an idea of mine. Of course you can use your own values.
*/
public function GetDestHeight takes destructable d returns real
return GetDestructableOccluderHeight(d)// Other example: return 72.
endfunction
/**
* Again it's up to you to figure out a fictional item height.
*/
public function GetItemHeight takes item i returns real
return 16.
endfunction
public function GetDestCollisionSize takes destructable d returns real
return 43.2
endfunction
public function GetItemCollisionSize takes item i returns real
return 18.
endfunction
/**
* 4. API
* ¯¯¯¯¯¯
*/
//! novjass
// Custom type Missile for your projectile needs.
struct Missile extends array
// Constants:
// ==========
readonly static constant real HIT_BOX = (2./3.)
// • Fictional hit box for homing missiles.
// while 0 would be the toe and 1 the head of a unit.
// Available creators:
// ===================
static method create takes real x, real y, real z, real angleInRadians, real distanceToTravel, real endZ returns Missile
// • Converts arguments to fit into createEx, then calls createEx.
static method createXYZ takes real x, real y, real z, real impactX, real impactY, real impactZ returns Missile
// • Converts arguments to fit into createEx, then calls createEx.
// Available destructors:
// ======================
//
return true
// • Core destructor.
// • Returning true in any of the interface methods of the MissileStruct module
// will destroy that instance instantly.
method destroy takes nothing returns nothing
// • Destroys the missile during the next timer callback.
method terminate takes nothing returns nothing
// • Destroys the missile instantly.
// Fields you can set and read directly:
// =====================================
//
unit source
unit target // For homing missiles.
real distance // Distance traveled.
player owner // Pseudo owner for faster onCollide evaluation. The proper dummy owner remains PLAYER_NEUTRAL_PASSIVE.
real speed // Vector lenght for missile movement in plane x / y. ( DOES NOT TAKE THE TIMER TIMEOUT IN ACCOUNT )
real acceleration
real damage
real turn // Set a turn rate for missiles.
integer data // For data transfer set and read data.
boolean recycle // Is automatically set to true, when a Missile reaches it's destination.
boolean wantDestroy // Set wantDestroy to true, to destroy a missile during the next timer callback.
// Neither collision nor collisionZ accept values below zero.
real collision // Collision size in the x-y-z axes
// Fields you can only read:
// =========================
readonly SpecialEffect effect
readonly boolean allocated
// Position members for you needs.
readonly MissilePosition origin// Grants access to readonly members of MissilePosition,
readonly MissilePosition impact// which are "x", "y", "z", "angle", "distance", "slope" and the pitch angle "alpha".
// Furthermore method origin.move(x, y, z) and impact.move(x, y, z).
readonly real terrainZ
readonly real x
readonly real y
readonly real z
readonly real angle// Current angle in radians.
// Method operators for set and read:
// ==================================
//
method operator model= takes string modelFile returns nothing
method operator model takes nothing returns string
// • For multiple effects manipulate "this.effect" directly instead.
method operator scale= takes real value returns nothing
method operator scale takes nothing returns real
// • Set and read the scaling of 'this.effect'.
method operator curve= takes real value returns nothing
method operator curve takes nothing returns real
// • Enables curved movement for your missile. ( Pass in radians, NOT degrees )
// • Do not pass in PI/2.
method operator arc= takes real value returns nothing
method operator arc takes nothing returns real
// • Enables arcing movement for your missile. ( Pass in radians, NOT degrees )
// • Do not pass in PI/2.
method operator spin= takes real rps returns nothing
method operator spin takes nothing returns real
// • Spin in rad/sec
// Methods for missile movement:
// =============================
//
method bounce takes nothing returns nothing
// • Moves the MissilePosition "origin" to the current missile coordinates.
// • Resets the distance traveled to 0.
method deflect takes real tx, real ty returns nothing
// • Deflect the missile towards tx, ty. Then calls bounce.
method deflectEx takes real tx, real ty, real tz returns nothing
// • Deflect the missile towards tx, ty, tz. Then calls bounce.
method flightTime2Speed takes real duration returns nothing
// • Converts a fly time to a vector lenght for member "speed".
// • Does not take acceleration into account. ( Disclaimer )
method setMovementSpeed takes real value returns nothing
// • Converts Warcraft III movement speed to a vector lenght for member "speed".
// Methods for missile collision: ( all are hashtable entries )
// ==============================
// By default a widget can only be hit once per missile.
//
method hitWidget takes widget w returns nothing
// • Saves a widget in the memory as hit by this instance.
method hasHitWidget takes widget w returns boolean
// • Returns true, if "w" has been hit by this instance.
method removeHitWidget takes widget w returns nothing
// • Removes a widget from this missile's memory for widget collision. ( Can hit "w" again )
method flushHitWidgets takes nothing returns nothing
// • Flushes a missile's memory for widget collision. ( All widgets can be hit again )
method enableHitAfter takes widget w, real seconds returns nothing
// • Automatically calls removeHitWidget(w) after "seconds" time. ( Can hit "w" again after given time )
// Module MissileStruct:
// =====================
//
module MissileLaunch // ( optional )
module MissileStruct
// • Enables the entire missile interface for that struct.
// Interface in structs: ( Must implement module MissileStruct )
// =====================
//
// • Write one, many or all of the static method below into your struct.
// • Missiles launched in this struct will run those methods, when their events fire.
//
// • All of those static methods must return a boolean.
// a) return true --> destroys the missile instance instantly.
// b) return false --> keep on flying.
// Available static method:
// ========================
//
static method onCollide takes Missile missile, unit hit returns boolean
// • Runs for units in collision range of a missile.
static method onDestructable takes Missile missile, destructable hit returns boolean
// • Runs for destructables in collision range of a missile.
static method onItem takes Missile missile, item hit returns boolean
// • Runs for items in collision range of a missile.
static method onTerrain takes Missile missile returns boolean
// • Runs when a missile collides with the terrain. ( Ground and cliffs )
static method onFinish takes Missile missile returns boolean
// • Runs only when a missile reaches it's destination.
// • However does not run, if a Missile is destroyed in another method.
static method onPeriod takes Missile missile returns boolean
// • Runs every Missile_TIMER_TIMEOUT seconds.
static method onRemove takes Missile missile returns boolean
// • Runs when a missile is destroyed.
// • Unlike onFinish, onRemove will runs ALWAYS when a missile is destroyed!!!
//
// For onRemove the returned boolean has a unique meaning:
// • Return true will recycle this missile delayed.
// • Return false will recycle this missile right away.
static method launch takes Missile toLaunch returns nothing
// • Well ... Launches this Missile.
// • Missile "toLaunch" will behave as declared in the struct. ( static methods from above )
// Misc: ( From the global setup )
// =====
//
// Constants:
// ==========
//
public constant real TIMER_TIMEOUT
public constant real MAXIMUM_COLLISION_SIZE
public constant boolean USE_COLLISION_Z_FILTER
readonly static constant real HIT_BOX
// Functions:
// ==========
//
public function GetLocZ takes real x, real y returns real
public function GetUnitHeight takes unit u returns real
public function GetDestHeight takes destructable d returns real
public function GetItemHeight takes item i returns real
public function GetDestCollisionSize takes destructable d returns real
public function GetItemCollisionSize takes item i returns real
//========================================================================
// Missile system. Make changes carefully.
//========================================================================
//! endnovjass
// Hello and welcome to Missile.
// I wrote a guideline for every piece of code inside Missile, so you
// can easily understand how the it gets compiled and evaluated.
//
// Let's go!
private keyword Init
globals
// Core constant handle variables of Missile.
private constant trigger CORE = CreateTrigger()
private constant trigger MOVE = CreateTrigger()
private constant timer TMR = CreateTimer()
private constant location LOC = Location(0., 0.)
private constant rect RECT = Rect(0., 0., 0., 0.)
private constant group GROUP = CreateGroup()
// For starting and stopping the timer.
private integer active = 0
// Arrays for data structure.
private integer array instances
private Missile array missileList
private boolexpr array expression
private triggercondition array condition
private unit tempUnit
private item tempItem
private destructable tempDest
private TableArray table
endglobals
public function GetLocZ takes real x, real y returns real
call MoveLocation(LOC, x, y)
return GetLocationZ(LOC)
endfunction
/*===============================================================================================*/
/*
* One allocator for the whole library
*/
private struct Node extends array
static if LIBRARY_Alloc then
implement optional Alloc
else
/*
* Credits to MyPad for the allocation algorithm
*/
private static thistype array stack
static method allocate takes nothing returns thistype
local thistype node = stack[0]
if stack[node] == 0 then
static if LIBRARY_ErrorMessage then
debug call ThrowError(node == (JASS_MAX_ARRAY_SIZE - 1), "Missile", "allocate()", "thistype", node, "Overflow")
endif
set node = node + 1
set stack[0] = node
else
set stack[0] = stack[node]
set stack[node] = 0
endif
return node
endmethod
method deallocate takes nothing returns nothing
static if LIBRARY_ErrorMessage then
debug call ThrowError(this == 0, "Missile", "deallocate()", "thistype", 0, "Null node")
debug call ThrowError(stack[this] > 0, "Missile", "deallocate()", "thistype", this, "Double-free")
endif
set stack[this] = stack[0]
set stack[0] = this
endmethod
endif
endstruct
// Simple trigonometry.
struct MissilePosition extends array
readonly real x
readonly real y
readonly real z
readonly real angle
readonly real distance
readonly real square
readonly real slope
readonly real alpha
// Creates an origin - impact link.
private thistype ref
private static method math takes thistype a, thistype b returns nothing
local real dx
local real dy
loop
set dx = b.x - a.x
set dy = b.y - a.y
set dx = dx*dx + dy*dy
set dy = SquareRoot(dx)
exitwhen dx != 0. and dy != 0.
set b.x = b.x + .01
set b.z = b.z - GetLocZ(b.x -.01, b.y) + GetLocZ(b.x, b.y)
endloop
set a.square = dx
set a.distance = dy
set a.angle = Atan2(b.y - a.y, b.x - a.x)
set a.slope = (b.z - a.z)/dy
set a.alpha = Atan(a.slope)
// Set b.
if b.ref == a then
set b.angle = a.angle + bj_PI
set b.distance = dy
set b.slope = -a.slope
set b.alpha = -a.alpha
set b.square = dx
endif
endmethod
static method link takes thistype a, thistype b returns nothing
set a.ref = b
set b.ref = a
call math(a, b)
endmethod
method move takes real toX, real toY, real toZ returns nothing
set this.x = toX
set this.y = toY
set this.z = toZ + GetLocZ(toX, toY)
if this.ref != this then
call math(this, this.ref)
endif
endmethod
static method create takes real x, real y, real z returns MissilePosition
local thistype this = Node.allocate()
set this.ref = this
call this.move(x, y, z)
return this
endmethod
method destroy takes nothing returns nothing
call Node(this).deallocate()
endmethod
endstruct
struct MissileList extends array
method operator missile takes nothing returns Missile
return this
endmethod
implement StaticList
endstruct
struct Missile extends array
// Attach point name for effects created by model=.
readonly static constant string ORIGIN = "origin"
// Set a ficitional hit box in range of 0 to 1,
// while 0 is the toe and 1 the head of a unit.
readonly static constant real HIT_BOX = (2./3.)
// Checks for double launching. Throws an error message.
debug boolean launched
// The position of a missile using curve= does not
// match the position used by Missile's trigonometry.
// Therefore we need this member two times.
// Readable x / y / z for your needs and posX / posY for cool mathematics.
private real posX
private real posY
private real dist// distance
// Readonly members:
// =================
//
// Prevents a double free case.
readonly boolean allocated
readonly SpecialEffect effect
// Position members for your needs.
readonly MissilePosition origin// Grants access to readonly members of MissilePosition,
readonly MissilePosition impact// which are "x", "y", "z", "angle", "distance", "slope" and "alpha".
readonly real terrainZ
readonly real angle// Current angle
readonly real prevX
readonly real prevY
readonly real prevZ
// Collision detection type. ( Evaluated new in each loop )
readonly integer collisionType// Current collision type ( circular or rectangle )
unit source
unit target // For homing missiles.
real distance // Distance traveled.
player owner // Pseudo owner for faster onCollide evaluation. The proper dummy owner is PLAYER_NEUTRAL_PASSIVE.
real speed // Vector length for missile movement in plane x / y.
real acceleration
real damage
integer data // For data transfer set and read data.
boolean recycle // Is set to true, when a Missile reaches it's destination.
real turn // Sets a turn rate for a missile.
real collision // Collision size in plane x / y.
private static method onInsert takes thistype node returns nothing
call MissileList.pushBack(node)
endmethod
private static method onRemove takes thistype node returns nothing
call MissileList.remove(node)
endmethod
implement List
method operator x takes nothing returns real
return this.effect.x
endmethod
method operator y takes nothing returns real
return this.effect.y
endmethod
method operator z takes nothing returns real
return this.effect.height
endmethod
private string path
method operator model= takes string file returns nothing
call this.effect.kill(DELAYED_MISSILE_DEATH_ANIMATION_TIME, false)
// null and ""
if StringLength(file) > 0 then
call BlzSetSpecialEffectScale(this.effect.addModel(file), this.scaling)
set this.path = file
else
set this.path = null
endif
endmethod
method operator model takes nothing returns string
return this.path
endmethod
readonly real open
// Enables curved movement for your missile.
// Remember that the result of Tan(PI/2) is infinity.
method operator curve= takes real value returns nothing
set this.open = Tan(value)*this.origin.distance/4
endmethod
method operator curve takes nothing returns real
return Atan(4*this.open/this.origin.distance)
endmethod
readonly real height
// Enables arcing movement for your missile.
method operator arc= takes real value returns nothing
set this.height = Tan(value)*this.origin.distance/4
endmethod
method operator arc takes nothing returns real
return Atan(4*this.height/this.origin.distance)
endmethod
private real spinRate
private real currentRoll
method operator spin= takes real value returns nothing
set this.spinRate = value*TIMER_TIMEOUT
endmethod
method operator spin takes nothing returns real
return this.spinRate/TIMER_TIMEOUT
endmethod
private real scaling
method operator scale= takes real value returns nothing
call this.effect.resetIterator()
loop
exitwhen this.effect.moveIterator()
call BlzSetSpecialEffectScale(this.effect.currentHandle(), value)
endloop
set this.scaling = value
endmethod
method operator scale takes nothing returns real
return this.scaling
endmethod
method bounce takes nothing returns nothing
call this.origin.move(this.x, this.y, this.z)
set this.dist = 0.
endmethod
method deflect takes real tx, real ty returns nothing
local real a = 2.*Atan2(ty - this.y, tx - this.x) + bj_PI - this.angle
call this.impact.move(x + (origin.distance - this.dist)*Cos(a), this.y + (this.origin.distance - this.dist)*Sin(a), this.impact.z)
call this.bounce()
endmethod
method deflectEx takes real tx, real ty, real tz returns nothing
call this.impact.move(this.impact.x, this.impact.y, tz)
call this.deflect(tx, ty)
endmethod
method flightTime2Speed takes real duration returns nothing
set this.speed = RMaxBJ(0.00000001, (this.origin.distance - this.dist)*Missile_TIMER_TIMEOUT/RMaxBJ(0.00000001, duration))
endmethod
method setMovementSpeed takes real value returns nothing
set this.speed = value*Missile_TIMER_TIMEOUT
endmethod
boolean wantDestroy// For "true" a missile is destroyed on the next timer callback. 100% safe.
method destroy takes nothing returns nothing
set this.wantDestroy = true
endmethod
// Instantly destroys a missile.
method terminate takes nothing returns nothing
if this.allocated then
set this.allocated = false
call this.impact.destroy()
call this.origin.destroy()
call table[this].flush()
set this.source = null
set this.target = null
set this.owner = null
call this.effect.kill(DELAYED_MISSILE_DEATH_ANIMATION_TIME, true)
call Node(this).deallocate()
call remove(this)
endif
endmethod
// Runs in createEx.
private method resetMembers takes nothing returns nothing
set this.path = null
set this.scaling = 1.
set this.speed = 0.
set this.acceleration = 0.
set this.spinRate = 0.
set this.distance = 0.
set this.dist = 0.
set this.height = 0.
set this.turn = 0.
set this.open = 0.
set this.collision = 0.
set this.collisionType = 0
set this.stackSize = 0
set this.wantDestroy = false
set this.recycle = false
endmethod
static method create takes real x, real y, real z, real angle, real distance, real impactZ returns thistype
local real impactX = x + distance*Cos(angle)
local real impactY = y + distance*Sin(angle)
local thistype this = Node.allocate()
call this.resetMembers()
set this.effect = SpecialEffect.create(x, y, z + GetLocZ(x, y))
set this.origin = MissilePosition.create(x, y, z)
set this.impact = MissilePosition.create(impactX, impactY, impactZ)
call MissilePosition.link(this.origin, this.impact)
set this.currentRoll = 0.
set this.posX = x
set this.posY = y
set this.angle = this.origin.angle
set this.allocated = true
debug set this.launched = false
return this
endmethod
static method createXYZ takes real x, real y, real z, real impactX, real impactY, real impactZ returns thistype
local real dx = impactX - x
local real dy = impactY - y
local real dz = impactZ - z
return create(x, y, z, Atan2(dy, dx), SquareRoot(dx*dx + dy*dy), impactZ)
endmethod
// Missile motion takes place every Missile_TIMER_TIMEOUT
// before accessing each active struct.
static Missile temp = 0
static method move takes nothing returns boolean
local integer loops = 0 // Current iteration.
local integer limit = 150 // Set iteration border per trigger evaluation to avoid hitting the operation limit.
local thistype this = thistype.temp
local MissilePosition p
local real a
local real d
local real roll
local unit u
local real newX
local real newY
local real newZ
local real prevAbsZ
local real vel
local real point
loop
exitwhen this == MissileList.head or loops == limit
set p = this.origin
// Save previous, respectively current missile position.
set this.prevX = this.x
set this.prevY = this.y
set this.prevZ = this.z
set prevAbsZ = this.effect.z
// Evaluate the collision type.
set vel = this.speed
set this.speed = vel + this.acceleration
if vel < this.collision*Missile_COLLISION_ACCURACY_FACTOR then
set this.collisionType = Missile_COLLISION_TYPE_CIRCLE
else
set this.collisionType = Missile_COLLISION_TYPE_RECTANGLE
endif
// Update missile guidance to its intended target.
set u = this.target
if u != null then
if 0 == GetUnitTypeId(u) then
set this.target = null
else
call p.move(this.prevX, this.prevY, this.prevZ)
call this.impact.move(GetUnitX(u), GetUnitY(u), GetUnitFlyHeight(u) + GetUnitHeight(u)*Missile.HIT_BOX)
set this.dist = 0
set this.height = 0
set this.curve = 0
endif
endif
set a = p.angle
// Update the missile facing angle depending on the turn ratio.
if 0. != this.turn and Cos(this.angle - a) < Cos(this.turn) then
if 0. > Sin(a - this.angle) then
set this.angle = this.angle - this.turn
else
set this.angle = this.angle + this.turn
endif
else
set this.angle = a
endif
// Update the missile position on the parabola.
set d = p.distance// origin - impact distance.
set this.recycle = this.dist + vel >= d
if this.recycle then
set point = d
set this.distance = this.distance + d - this.dist
set newX = this.impact.x
set newY = this.impact.y
set newZ = this.impact.z
set this.posX = newX
set this.posY = newY
else
set this.distance = this.distance + vel
set point = this.dist + vel
set newX = this.posX + vel*Cos(this.angle)
set newY = this.posY + vel*Sin(this.angle)
set this.posX = newX
set this.posY = newY
// Update point(x/y) if a curving trajectory is defined.
if 0. != this.open and u == null then
set vel = 4*this.open*point*(d - point)/p.square
set a = this.angle + bj_PI/2
set newX = newX + vel*Cos(a)
set newY = newY + vel*Sin(a)
if vel < 0.00 then
set a = Atan2(newY - this.prevY, newX - this.prevX) + bj_PI
else
set a = Atan2(newY - this.prevY, newX - this.prevX)
endif
else
set a = this.angle
endif
// Update pos z if an arc or height is set.
if 0. == this.height and 0. == p.alpha then
set newZ = p.z
else
set newZ = p.z + p.slope*point
if 0. != this.height and u == null then
set newZ = newZ + (4*this.height*point*(d - point)/p.square)
endif
endif
endif
set this.dist = point
set this.terrainZ = GetLocZ(newX, newY)
if this.open < 0.00 then
set roll = Atan2(this.open, this.height) + bj_PI
else
set roll = Atan2(this.open, this.height)
endif
set this.currentRoll = this.currentRoll - this.spinRate
// Set missile position and orientation
call this.effect.move(newX, newY, newZ)
call this.effect.setOrientation(a, -Atan2(newZ - prevAbsZ, vel), this.currentRoll - roll)
set loops = loops + 1
set this = MissileList(this).next
endloop
set u = null
set thistype.temp = this
return this == MissileList.head
endmethod
// Widget collision API:
// =====================
//
// Runs automatically on widget collision.
method hitWidget takes widget w returns nothing
if w != null then
set table[this].widget[GetHandleId(w)] = w
endif
endmethod
// All widget which have been hit return true.
method hasHitWidget takes widget w returns boolean
return table[this].handle.has(GetHandleId(w))
endmethod
// Removes a widget from the missile's memory of hit widgets. ( This widget can be hit again )
method removeHitWidget takes widget w returns nothing
if w != null then
call table[this].handle.remove(GetHandleId(w))
endif
endmethod
// Flushes a missile's memory for collision. ( All widgets can be hit again )
method flushHitWidgets takes nothing returns nothing
call table[this].flush()
endmethod
// Tells missile to call removeHitWidget(w) after "seconds" time.
// Does not apply to widgets, which are already hit by this missile.
readonly integer stackSize
method enableHitAfter takes widget w, real seconds returns nothing
local integer id = GetHandleId(w)
local Table t
if w != null then
set t = table[this]
if not t.has(id) then
set t[id] = stackSize
set t[stackSize] = id
set stackSize = stackSize + 1
endif
set t.real[id] = seconds
endif
endmethod
method updateStack takes nothing returns nothing
local integer dex = 0
local integer id
local real time
local Table t
loop
exitwhen dex == stackSize
set t = table[this]
set id = t[dex]
set time = t.real[id] - Missile_TIMER_TIMEOUT
if time <= 0. or not t.handle.has(id) then
set stackSize = stackSize - 1
set id = t[stackSize]
set t[dex] = id
set t[id] = dex
// Enables hit.
call t.handle.remove(id)
// Remove data from stack.
call t.real.remove(id)
call t.remove(id)
call t.remove(stackSize)
else
set t.real[id] = time
set dex = dex + 1
endif
endloop
endmethod
// Widget collision code:
// ======================
//
private static boolean circle = true
//
// Rectangle collision for fast moving missiles with small collision radius.
//
// Runs for destructables and items in a rectangle.
// Checks if widget w is in collision range of a missile.
// Is not precise in z - axis collision.
private method isWidgetInRange takes real x, real y, real ws, real ms returns boolean
set x = x - this.x
set y = y - this.y
set ws = ws + ms
return x*x + y*y <= ws*ws
endmethod
private method isWidgetInRect takes real x, real y, real ws, real ms returns boolean
local real dx = this.x - this.prevX
local real dy = this.y - this.prevY
local real s = (dx*(x - this.prevX) + dy*(y - this.prevY))/(dx*dx + dy*dy)
if s < 0. then
set s = 0.
elseif s > 1 then
set s = 1.
endif
set dx = (this.prevX + s*dx) - x
set dy = (this.prevY + s*dy) - y
set ws = ws + ms
return dx*dx + dy*dy <= ws*ws
endmethod
private method isUnitInRect takes real x, real y returns boolean
local real dx = this.x - this.prevX
local real dy = this.y - this.prevY
local real s = (dx*(x - this.prevX) + dy*(y - this.prevY))/(dx*dx + dy*dy)
if s < 0. then
set s = 0.
elseif s > 1 then
set s = 1.
endif
return IsUnitInRangeXY(tempUnit, this.prevX + s*dx, this.prevY + s*dy, this.collision)
endmethod
static if Missile_USE_COLLISION_Z_FILTER then
private method checkZCollision takes real z, real wz returns boolean
set z = z - this.terrainZ
return z + wz >= this.z - this.collision and z <= this.z + this.collision
endmethod
endif
private method isWidgetInCollision takes widget w, real ws, real wz, real ms returns boolean
local real x = GetWidgetX(w)
local real y = GetWidgetY(w)
static if Missile_USE_COLLISION_Z_FILTER then
if circle then
return this.checkZCollision(GetLocZ(x, y), wz) and this.isWidgetInRange(x, y, ws, ms)
endif
return this.checkZCollision(GetLocZ(x, y), wz) and this.isWidgetInRect(x, y, ws, ms)
else
if circle then
return this.isWidgetInRange(x, y, ws, ms)
endif
return this.isWidgetInRect(x, y, ws, ms)
endif
endmethod
private method isUnitInCollision takes nothing returns boolean
static if Missile_USE_COLLISION_Z_FILTER then
local real x = GetUnitX(tempUnit)
local real y = GetUnitY(tempUnit)
if circle then
return this.checkZCollision(GetUnitFlyHeight(tempUnit) + GetLocZ(x, y), GetUnitHeight(tempUnit)) and IsUnitInRangeXY(tempUnit, this.x, this.y, this.collision)
endif
return this.checkZCollision(GetUnitFlyHeight(tempUnit) + GetLocZ(x, y), GetUnitHeight(tempUnit)) and this.isUnitInRect(x, y)
else
if circle then
return IsUnitInRangeXY(tempUnit, this.x, this.y, this.collision)
endif
return this.isUnitInRect(GetUnitX(tempUnit), GetUnitY(tempUnit))
endif
endmethod
//
// Runs for every enumerated destructable.
// • Directly filters out already hit destructables.
// • Distance formula based on the Pythagorean theorem.
//
static method destFilter takes nothing returns boolean
if temp.allocated then
set tempDest = GetFilterDestructable()
if not temp.hasHitWidget(tempDest) and temp.isWidgetInCollision(tempDest, GetDestCollisionSize(tempDest), GetDestHeight(tempDest), temp.collision) then
set table[temp].destructable[GetHandleId(tempDest)] = tempDest
return true
endif
endif
return false
endmethod
//
// Runs for every enumerated item.
// • Directly filters out already hit items.
// • Distance formula based on the Pythagorean theorem.
// • Items have a fix collision size of 16.
//
static method itemFilter takes nothing returns boolean
if temp.allocated then
set tempItem = GetFilterItem()
if not temp.hasHitWidget(tempItem) and temp.isWidgetInCollision(tempItem, GetItemCollisionSize(tempItem), GetItemHeight(tempItem), RMaxBJ(temp.collision, 16.)) then
set table[temp].item[GetHandleId(tempItem)] = tempItem
return true
endif
endif
return false
endmethod
//
// Runs for every enumerated units.
// • Filters out units which are not in collision range in plane x / y.
//
static method unitFilter takes nothing returns boolean
if temp.allocated then
set tempUnit = GetFilterUnit()
if not temp.hasHitWidget(tempUnit) and temp.isUnitInCollision() then
set table[temp].unit[GetHandleId(tempUnit)] = tempUnit
return true
endif
endif
return false
endmethod
/*
* Proper rect preparation.
*/
private method prepareRectRectangle takes nothing returns nothing
local real x1 = RMinBJ(this.prevX, this.x)
local real y1 = RMinBJ(this.prevY, this.y)
local real d = this.collision + Missile_MAXIMUM_COLLISION_SIZE
call SetRect(RECT, x1 - d, y1 - d, this.prevX + this.x - x1 + d, this.prevY + this.y - y1 + d)
endmethod
private method prepareRect takes nothing returns nothing
local real d = this.collision + Missile_MAXIMUM_COLLISION_SIZE
set circle = this.collisionType == Missile_COLLISION_TYPE_CIRCLE
if circle then
call SetRect(RECT, this.x - d, this.y - d, this.x + d, this.y + d)
else
call this.prepareRectRectangle()
endif
set thistype.temp = this
endmethod
/*
* 5.) API for the MissileStruct iteration.
*/
method checkUnitCollision takes code filter returns nothing
if this.allocated and this.collision > 0. then
set thistype.temp = this
set circle = this.collisionType == Missile_COLLISION_TYPE_CIRCLE
if circle then
call GroupEnumUnitsInRange(GROUP, this.x, this.y, this.collision + Missile_MAXIMUM_COLLISION_SIZE, Filter(filter))
else
call this.prepareRectRectangle()
call GroupEnumUnitsInRect(GROUP, RECT, Filter(filter))
endif
endif
endmethod
method checkDestCollision takes code filter returns nothing
if this.allocated and this.collision > 0. then
call this.prepareRect()
call EnumDestructablesInRect(RECT, Filter(filter), null)
endif
endmethod
method checkItemCollision takes code filter returns nothing
if this.allocated and this.collision > 0. then
call this.prepareRect()
call EnumItemsInRect(RECT, Filter(filter), null)
endif
endmethod
private static method onScopeInit takes nothing returns nothing
static if LIBRARY_ErrorMessage then
debug call ThrowError((Missile_MAXIMUM_COLLISION_SIZE < 0), "Missile", "DEBUG_MISSILE", "collision", 0, "Global setup for public real MAXIMUM_COLLISION_SIZE is incorrect, below zero! This map currently can't use Missile!")
endif
call TriggerAddCondition(MOVE, Condition(function thistype.move))
set table = TableArray[JASS_MAX_ARRAY_SIZE]
endmethod
implement Init
endstruct
// Boolean expressions per struct:
// ===============================
//
// Condition function for the core trigger evaluation. ( Runs for all struct using module MissileStruct )
private function MissileCreateExpression takes integer structId, code func returns nothing
set expression[structId] = Condition(func)
endfunction
// Creates a collection for a struct. ( Runs for all struct using module MissileStruct )
private function MissileCreateCollection takes integer structId returns nothing
local Missile node = Node.allocate()
call Missile.makeHead(node) // Make a circular list
set missileList[structId] = node
endfunction
// Core:
// =====
//
// Fires every Missile_TIMER_TIMEOUT.
private function Fire takes nothing returns nothing
local integer i = 0
// Move all launched missiles.
set Missile.temp = MissileList.front
loop
exitwhen TriggerEvaluate(MOVE)
exitwhen i == 60// Moved over 8910 missiles, something buggy happened.
set i = i + 1 // This case is impossible, hence never happens. But if I'm wrong, which also never happens
endloop // then the map 100% crashes. i == 66 will prevent the game crash and Missile will start to
// to debug itself over the next iterations.
// Access all structs using module MissileStruct.
static if DEBUG_MODE and LIBRARY_ErrorMesssage then
if not TriggerEvaluate(CORE) then
call ThrowWarning(true, "Missile", "Fire", "op limit", 0, /*
*/"You just ran into a op limit!
The op limit protection of Missile is insufficient for your map.
The code of the missile module can't run in one thread and must be splitted.
If unsure make a post in the official 'The Hive Workshop' forum thread of Missile!")
endif
else
call TriggerEvaluate(CORE)
endif
endfunction
// Conditionally starts the timer.
private function StartPeriodic takes integer structId returns nothing
if 0 == instances[structId] then
if 0 == active then
call TimerStart(TMR, Missile_TIMER_TIMEOUT, true, function Fire)
endif
set active = active + 1
set condition[structId] = TriggerAddCondition(CORE, expression[structId])
endif
set instances[structId] = instances[structId] + 1
endfunction
// Conditionally stops the timer in the next callback.
private function StopPeriodic takes integer structId returns nothing
if condition[structId] != null then
set instances[structId] = instances[structId] - 1
if 0 == instances[structId] then
call TriggerRemoveCondition(CORE, condition[structId])
set condition[structId] = null
set active = active - 1
if 0 == active then
call PauseTimer(TMR)
endif
endif
endif
endfunction
// Modules:
// ========
//
// You want to place module MissileLaunch at the very top of your struct.
module MissileLaunch
static method launch takes Missile missile returns nothing
static if LIBRARY_ErrorMessage then
debug call ThrowError(missile.launched, "thistype", "launch", "missile.launched", missile, "This missile was already launched before!")
endif
debug set missile.launched = true
call missileList[thistype.typeid].pushBack(missile)
call StartPeriodic(thistype.typeid)
endmethod
endmodule
module MissileTerminate
// Called from missileIterate. "P" to avoid code collision.
static method missileTerminateP takes Missile node returns nothing
if node.allocated then
static if thistype.onRemove.exists then
call thistype.onRemove(node)
endif
call node.terminate()
call StopPeriodic(thistype.typeid)
endif
endmethod
endmodule
// Allows you to inject missile in certain stages of the motion process.
module MissileAction
static if thistype.onCollide.exists then
private static method missileActionUnit takes nothing returns nothing
if Missile.unitFilter() and thistype.onCollide(Missile.temp, tempUnit) then
call missileTerminateP(Missile.temp)
endif
endmethod
endif
static if thistype.onItem.exists then
private static method missileActionItem takes nothing returns nothing
if Missile.itemFilter() and thistype.onItem(Missile.temp, tempItem) then
call missileTerminateP(Missile.temp)
endif
endmethod
endif
static if thistype.onDestructable.exists then
private static method missileActionDest takes nothing returns nothing
if Missile.destFilter() and thistype.onDestructable(Missile.temp, tempDest) then
call missileTerminateP(Missile.temp)
endif
endmethod
endif
// Runs every Missile_TIMER_TIMEOUT for this struct.
static method missileIterateP takes nothing returns boolean
local Missile node = missileList[thistype.typeid].front
loop
exitwhen node == missileList[thistype.typeid]
if node.wantDestroy then
call missileTerminateP(node)
else
if node.stackSize > 0 then
call node.updateStack()
endif
// Runs unit collision.
static if thistype.onCollide.exists then
call node.checkUnitCollision(function thistype.missileActionUnit)
endif
// Runs destructable collision.
static if thistype.onDestructable.exists then
call node.checkDestCollision(function thistype.missileActionDest)
endif
// Runs item collision.
static if thistype.onItem.exists then
call node.checkItemCollision(function thistype.missileActionItem)
endif
// Runs when the destination is reached.
if node.recycle and node.allocated then
static if thistype.onFinish.exists then
if thistype.onFinish(node) then
call missileTerminateP(node)
endif
else
call missileTerminateP(node)
endif
endif
// Runs on terrain collision.
static if thistype.onTerrain.exists then
if node.allocated and 0. > node.z and thistype.onTerrain(node) then
call missileTerminateP(node)
endif
endif
// Runs every Missile_TIMER_TIMEOUT.
static if thistype.onPeriod.exists then
if node.allocated and thistype.onPeriod(node) then
call missileTerminateP(node)
endif
endif
endif
set node = node.next
endloop
static if DEBUG_MODE and LIBRARY_ErrorMessage then
return true
else
return false
endif
endmethod
endmodule
module MissileStruct
implement MissileLaunch
implement MissileTerminate
implement MissileAction
private static method onInit takes nothing returns nothing
call MissileCreateCollection(thistype.typeid)
call MissileCreateExpression(thistype.typeid, function thistype.missileIterateP)
endmethod
endmodule
private module Init
private static method onInit takes nothing returns nothing
call Missile.onScopeInit()
endmethod
endmodule
// The end!
endlibrary
v3.2.0
- Fixed a bug regarding the pitch calculation
- Operator model= now applies the correct sfx scale to the added sfx
- Added a new feature:
- The MissileStruct module now generates the least amount of code possible
- Optimized the widget enumeration on collision process
- Other changes
v3.1.0
- First release since the legacy version
- Fixed a bug regarding the pitch calculation
- Operator model= now applies the correct sfx scale to the added sfx
- Added a new feature:
spin
that allows the Missile to spin/roll measured in radians per second- The MissileStruct module now generates the least amount of code possible
- Optimized the widget enumeration on collision process
- Other changes
v3.1.0
- First release since the legacy version
Last edited: