• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] Missile

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Missile
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.


missile_roll_test_2-gif.359578


missile_roll_test-gif.359579

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: 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:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
Updated to 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: 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
 
Level 20
Joined
Jul 10, 2009
Messages
477
Hey AGD,

I tried to import Missile into an empty w3 map today (alongside the required libraries). Table, Special Effect and Linked List could be properly imported, but the editor crashes after importing Missile upon Syntax Check (Ctrl+F10).

I've attached a sample map, would you mind to take a look? (you would need to copy the Missile code into the map and press ctrl+F10).

I'm using patch warcraft 1.32.7 and have not touched any library configuration. Just copy pasted the original code to the map.

Best regards,
Eikonium

Edit: Found the issue. The library does only crash the editor, if copied to a Script (Ctrl+U). Copy to a trigger instead to avoid crashing (Ctrl+T -> Convert to custom text).
 

Attachments

  • Missile Bug.w3m
    44.3 KB · Views: 36
Last edited:
Top