• 🏆 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] [Snippet] LockBone

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
Code
JASS:
library LockBone uses optional TimerUtils, optional Table, optional MissileRecycler
    /* v1.6 */
    
    globals
        // If only you don't use MissileRecycler
        private constant integer DUMMY_ID = 'dumi'
    endglobals
    
    /*
        Description
        ¯¯¯¯¯¯¯¯¯¯¯
            Allows you to lock a unit's body part to face certain angle or point.
            
            
        External Dependencies
        ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
            (Requires) - Nothing
            (Optional) - These libs are completely optional. But it's a win win to have all of these to go with the system.
            
                1. This lib allows the system to save data into timers, which is useful for struct based spells/systems
                        | TimerUtils      @ wc3c.net/showthread.php?t=101322
                    
                2. This lib can ease the handling of hashtables as well as make it perfectly efficient uses of hashtables.
                        | Table           @ hiveworkshop.com/forums/jass-resources-412/snippet-new-table-188084/
                    
                3. This system uses a lot of dummy units. Unit creations are heavy process, and other problems as well.
                   Here is where dummy recycler comes important.
                        | MissileRecycler @ hiveworkshop.com/forums/jass-resources-412/system-missilerecycler-206086/
            
            
        API
        ¯¯¯
            struct LockBone
                
                1. Lock body part at given coordinate
                    | static method lockAtPoint takes unit whichUnit, string whichBone, real targetX, real targetY, real zOffset returns thistype
                        - targetX : x-axis of the target point
                        - targetY : y-axis of the target point
                        - zOffset : height level at which the bone will be locked
                
                2. Lock body part at given angle (in radian)
                    | static method lockAtAngle takes unit whichUnit, string whichBone, real radian, real hOffset, real vOffset returns thistype
                        - radian  : at what angle (in radian) unit's bone will be locked
                        - hOffset : horizontal offset
                        - vOffset : vertical offset
                            
                                 0 ............... -> lock point
                         unit <- | ______________: -> vOffset
                                  \_____________/
                                      hOffset
                                 
                3. Reset any body locking
                    | static method resetLock takes unit whichUnit returns nothing
            
                4. Bones part constants
                    BONE_PART_CHEST => rotate chest bone
                    BONE_PART_HEAD  => rotate head bone
                
                    
        Resource Link
        ¯¯¯¯¯¯¯¯¯¯¯¯¯
            hiveworkshop.com/forums/submissions-414/snippet-lockbone-259005/
    */
    
    globals
        constant string BONE_PART_CHEST  = "bone_chest"
        constant string BONE_PART_HEAD   = "bone_head"
        private constant real   INTERVAL = 0.03125
        private constant player PASSIVE  = Player(PLAYER_NEUTRAL_PASSIVE)
    endglobals
    
    struct LockBone
        
        private timer t
        private unit  u
        private unit  d
        
        private real cos
        private real sin
        
        private static real MapMaxX
        private static real MapMaxY
        private static real MapMinX
        private static real MapMinY
        
        static if LIBRARY_Table then
            private static Table Ht
        else
            private static hashtable Ht
        endif
        
        private static method onPeriodic takes nothing returns nothing
            
            local thistype this
            local real x
            local real y
            
            static if LIBRARY_TimerUtils then
                set this = GetTimerData(GetExpiredTimer())
            else
                static if LIBRARY_Table then
                    set this = Ht.integer[GetHandleId(GetExpiredTimer())]
                else
                    set this = LoadInteger(Ht, GetHandleId(GetExpiredTimer()), 0)
                endif
            endif
            set x = GetUnitX(.u)+.cos
            set y = GetUnitY(.u)+.sin
            if x > MapMinX and x < MapMaxX and y > MapMinY and y < MapMaxY then
                call SetUnitX(.d, x)
                call SetUnitY(.d, y)
            else
                call SetUnitPosition(.d, x, y)
            endif
            
        endmethod
        
        static method resetLock takes unit whichUnit returns nothing
        
            local thistype this
            local integer  hand = GetHandleId(whichUnit)
            
            static if LIBRARY_Table then
                set this = Ht.integer[hand]
            else
                set this = LoadInteger(Ht, hand, 0)
            endif
            if this != 0 then
                call ResetUnitLookAt(.u)
                static if LIBRARY_MissileRecycler then
                    call RecycleMissile(.d)
                    call SetUnitX(.d, MapMaxX)
                    call SetUnitY(.d, MapMaxY)
                else
                    call RemoveUnit(.d)
                endif
                static if LIBRARY_TimerUtils then
                    call ReleaseTimer(.t)
                else
                    call DestroyTimer(.t)
                    static if LIBRARY_Table then
                        call Ht.integer.remove[GetHandleId(.t)]
                    else
                        call RemoveSavedInteger(Ht, GetHandleId(.t), 0)
                    endif
                endif
                static if LIBRARY_Table then
                    set Ht.integer[hand] = 0
                else
                    call SaveInteger(Ht, hand, 0, 0)
                endif
                call destroy()
                set .u = null
                set .d = null
                set .t = null
            endif
            
        endmethod
        
        static method lockAtPoint takes unit whichUnit, string whichBone, real targetX, real targetY, real zOffset returns thistype
        
            local thistype this
            
            static if LIBRARY_Table then
                set this = Ht.integer[GetHandleId(whichUnit)]
            else
                set this = LoadInteger(Ht, GetHandleId(whichUnit), 0)
            endif
            if this == 0 then
                set this = allocate()
                static if LIBRARY_Table then
                    set Ht.integer[GetHandleId(whichUnit)] = this
                else
                    call SaveInteger(Ht, GetHandleId(whichUnit), 0, this)
                endif
                
                static if LIBRARY_TimerUtils then
                    set .t = NewTimerEx(this)
                else
                    set .t = CreateTimer()
                    static if LIBRARY_Table then
                        set Ht.integer[GetHandleId(.t)] = this
                    else
                        call SaveInteger(Ht, GetHandleId(.t), 0, this)
                    endif
                endif
                
                static if LIBRARY_MissileRecycler then
                    set .d = GetRecycledMissile(0, 0, 0, 0)
                else
                    set .d = CreateUnit(PASSIVE, DUMMY_ID, 0, 0, 0)
                    static if not LIBRARY_AutoFly then
                        if UnitAddAbility(.d, 'Amrf') and UnitRemoveAbility(.d, 'Amrf') then
                        endif
                    endif
                endif
                
                set .u = whichUnit
                call UnitRemoveAbility(.d, 'Amov')
                call SetUnitLookAt(.u, whichBone, .d, 0, 0, 0)
            endif
            
            if TimerGetTimeout(.t) > 0 then
                call TimerStart(.t, 0, false, null)
                call PauseTimer(.t)
            endif
            call SetUnitFlyHeight(.d, zOffset, 0)
            call SetUnitX(.d, targetX)
            call SetUnitY(.d, targetY)
            
            return this
        endmethod
        
        static method lockAtAngle takes unit whichUnit, string whichBone, real radian, real hOffset, real vOffset returns thistype
        
            local thistype this
            
            static if LIBRARY_Table then
                set this = Ht.integer[GetHandleId(whichUnit)]
            else
                set this = LoadInteger(Ht, GetHandleId(whichUnit), 0)
            endif
            if this == 0 then
                set this = allocate()
                static if LIBRARY_Table then
                    set Ht.integer[GetHandleId(whichUnit)] = this
                else
                    call SaveInteger(Ht, GetHandleId(whichUnit), 0, this)
                endif
                
                static if LIBRARY_TimerUtils then
                    set .t = NewTimerEx(this)
                else
                    set .t = CreateTimer()
                    static if LIBRARY_Table then
                        set Ht.integer[GetHandleId(.t)] = this
                    else
                        call SaveInteger(Ht, GetHandleId(.t), 0, this)
                    endif
                endif
                
                static if LIBRARY_MissileRecycler then
                    set .d = GetRecycledMissile(0, 0, 0, 0)
                else
                    set .d = CreateUnit(PASSIVE, DUMMY_ID, 0, 0, 0)
                    static if not LIBRARY_AutoFly then
                        if UnitAddAbility(.d, 'Amrf') and UnitRemoveAbility(.d, 'Amrf') then
                        endif
                    endif
                endif
                
                set .u = whichUnit
                call UnitRemoveAbility(.d, 'Amov')
                call SetUnitLookAt(.u, whichBone, .d, 0, 0, 0)
            endif
            
            if TimerGetTimeout(.t) == 0 then
                call TimerStart(.t, INTERVAL, true, function thistype.onPeriodic)
            endif
            set .cos = hOffset*Cos(radian)
            set .sin = hOffset*Sin(radian)
            call SetUnitFlyHeight(.d, vOffset, 0)
            call SetUnitX(.d, GetUnitX(.u)+.cos)
            call SetUnitY(.d, GetUnitY(.u)+.sin)
            
            return this
        endmethod
        
        private static method onDeath takes nothing returns boolean
        
            local unit u = GetTriggerUnit()
            local thistype this
            
            static if LIBRARY_Table then
                set this = Ht.integer[GetHandleId(u)]
            else
                set this = LoadInteger(Ht, GetHandleId(u), 0)
            endif
            if this != 0 then
                call resetLock(u)
            endif
            set u = null
            
            return false
        endmethod
        
        private static method onInit takes nothing returns nothing
            
            local trigger t = CreateTrigger()
            
            static if LIBRARY_Table then
                set Ht = Table.create()
            else
                set Ht = InitHashtable()
            endif
            set MapMaxX = GetRectMaxX(bj_mapInitialPlayableArea)
            set MapMaxY = GetRectMaxY(bj_mapInitialPlayableArea)
            set MapMinX = GetRectMinX(bj_mapInitialPlayableArea)
            set MapMinY = GetRectMinY(bj_mapInitialPlayableArea)
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
            call TriggerAddCondition(t, Condition(function thistype.onDeath))
            
        endmethod
        
    endstruct
    
endlibrary

Demo
Demo spell
 
Last edited:
lookAtAngle should be able to be achieved just by forcing an angle relative to the units current position (but super far away).
JASS:
function SetDummyFacing takes unit u, real angle returns nothing
    call SetUnitLookAt(u, "Bone_Head", u, Cos(angle)*1000000., Sin(angle)*1000000., 0.)
endfunction
http://www.wc3c.net/showthread.php?t=105830

But using a dummy for lockAtPoint is fine.

And you should add "bone_head" and "bone_chest" as constants. Those are actually the only two inputs. "bone_chest" will lock the chest. Any other input (apart from null) and ones that don't start with "bone_chest" will lock the head. Sadly, we can't lock any other bones. :\
For more information.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
JASS:
function SetDummyFacing takes unit u, real angle returns nothing
    call SetUnitLookAt(u, "Bone_Head", u, Cos(angle)*1000000., Sin(angle)*1000000., 0.)
endfunction

That one is glitched if you use it too often (every 0.05 second for example) even with 0 blend time.

(apart from null)
What happen if we input null anyway? I haven't tested it.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
UnitIndexer API has changed. This will not compile with the current version.

You could also do a periodic check if the unit does exists GetUnitTypeId(unit) != 0
and store allocated units via Table and HandleId. ( Just an idea, UnitIndexer is also fine )

I guess this works in its current state. So we can approve it soon.
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
- I won't use UnitIndexer then, if the old indexer is not allowed. UnitDex hasn't been approved either. The last time I used it, it was still bugged.

- Those offsets will affect where the unit's bone will face. As example, you set zOffset to high value such as 500.0, so that the unit will face to the sky. But yeah, there's a mistake, we only need two offset, xyOffset and zOffset. Or better yet to call'em hOffset (horizontal) and vOffset (vertical).

- Not yet, as far as I recall. But I will test it before uploading the next update.

EDIT:
Btw, which dummy recycler lib should I use? Can I use Bribe's MissileRecycler?
 
Last edited:
Wouldnt it be possible to use GetWorldBounds over bj_mapInitialPlayableArea for some more room for the dummies.

For integrity check "have" natives could be used, for example:

JASS:
        private static method onDeath takes nothing returns boolean
     
            local unit u = GetTriggerUnit()
            local thistype this
         
            static if LIBRARY_Table then
                set this = Ht.integer[GetHandleId(u)]
            else
                set this = LoadInteger(Ht, GetHandleId(u), 0)
            endif
            if this != 0 then
                call resetLock(u)
            endif
            set u = null
         
            return false
        endmethod
can be ==>
JASS:
        private static method onDeath takes nothing returns boolean
     
            local unit u = GetTriggerUnit()
         
            static if LIBRARY_Table then
                if Ht.has(GetHandleId(u)) then
                    call resetLock(u)
                endif
            else
                if HaveSavedInteger(Ht, GetHandleId(u), 0) then
                    call resetLock(u)
                endif
            endif
         
            set u = null
            return false
        endmethod

0.03125 is maybe not required here, you could higher it a bit if you want.

JASS:
constant string BONE_PART_CHEST  = "bone_chest"
constant string BONE_PART_HEAD   = "bone_head"

^I personaly would do them static constants of the struct so access them like "LockBone.Chest", "LockBone.Head", but it does not really matter.

Optional requirements -> pretty cool.

Resource is good. :)
 
Top