1. Updated Resource Submission Rules: All model & skin resource submissions must now include an in-game screenshot. This is to help speed up the moderation process and to show how the model and/or texture looks like from the in-game camera.
    Dismiss Notice
  2. DID YOU KNOW - That you can unlock new rank icons by posting on the forums or winning contests? Click here to customize your rank or read our User Rank Policy to see a list of ranks that you can unlock. Have you won a contest and still haven't received your rank award? Then please contact the administration.
    Dismiss Notice
  3. Join the 6th Melee Mapping Contest for a chance to have your map featured in this year's Hive Cup!
    Dismiss Notice
  4. Shoot to thrill, play to kill. Sate your hunger with the 33rd Modeling Contest!
    Dismiss Notice
  5. Do you hear boss music? It's the 17th Mini Mapping Contest!
    Dismiss Notice
  6. Let your favorite entries duke it out in the 15th Techtree Contest Poll.
    Dismiss Notice
  7. Weave light to take you to your highest hopes - the 6th Special Effect Contest is here!
    Dismiss Notice
  8. Check out the Staff job openings thread.
    Dismiss Notice
Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

[vJASS] Missile

Discussion in 'Submissions' started by AGD, Jul 10, 2020.

  1. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    547
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    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.

    Added Roll preview

    [​IMG]

    [​IMG]
    Notice that the roll is now dependent on the arc and curve of the Missile.


    Missile.j
    Code (vJASS):
    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: Jul 17, 2020
  2. JAKEZINC

    JAKEZINC

    Joined:
    Aug 13, 2013
    Messages:
    1,605
    Resources:
    9
    Spells:
    9
    Resources:
    9
  3. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    8,285
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    A worthy compliment to your new Effect library. Both state of the art.
     
  4. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    547
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    Thanks for the generous compliments Bribe.
     
  5. AGD

    AGD

    Joined:
    Mar 29, 2016
    Messages:
    547
    Resources:
    13
    Spells:
    7
    Tutorials:
    1
    JASS:
    5
    Resources:
    13
    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
     
  6. Eikonium

    Eikonium

    Joined:
    Jul 10, 2009
    Messages:
    147
    Resources:
    2
    Maps:
    2
    Resources:
    2
    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).
     

    Attached Files:

    Last edited: Jul 26, 2020